[NEW] User profile and User card (#18194)
* Handle Avatar on Error * User Status Component * User Card Component * More variations * More Variations * Loading State * useTimezoneTime Hook * UTCClock Component * Fix BaseAvatar * getStatus Helper * Rewrite User info to reuse + Stories * Changes to open on Channels * ??? * Fix reactAdapters * Translation Provider * Fix lint * Improve loading state * WIP * UserCard and UserInfo Actions * Lint * Review * wip * Fix user info * Update fuselage * Update fuselage 0.13.0 * sad * sad[2] * fix camelCase paths * Fix Ghost menu * Fix some weird behaviors * Fix missing user ID on actions * Fix visual * Fix VerticalBar * Opss missing email field * Missing some fields * Update Fuselage * Update app/theme/client/imports/components/contextual-bar.css Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> * Remove Old userInfo template * UserCard Api * Update client/admin/customEmoji/CustomEmojiRoute.js Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> * Remove unused variables * Fix contextual-bar CSS selectors * Destructure additional props in UserInfoWithData * Remove obsolete props from VerticalBar * Remove commented code * Place useSetModal hook * Apply suggestions from code review Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> * Fiz markdown * Update client/components/basic/UserCard.js Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> * Update client/components/basic/avatar/UserAvatar.js Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> * rename base avatar Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat>pull/18294/head
parent
4b1b44a6c5
commit
42d41ba288
@ -1,215 +0,0 @@ |
||||
<template name="userEdit"> |
||||
<section class="contextual-bar__content"> |
||||
{{#unless canEditOrAdd}} |
||||
<p class="secondary-font-color">{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<form action="index.html" method="post" autocomplete="off"> |
||||
<div class="rc-form-group rc-form-group--small"> |
||||
<label class="rc-form-label"> |
||||
{{_ "Profile_picture"}} |
||||
</label> |
||||
<div class="rc-select-avatar"> |
||||
<div class="rc-select-avatar__preview"> |
||||
{{#if avatarPreview}} |
||||
{{#if $eq avatarPreview.service 'initials'}} |
||||
{{> avatar username=avatarPreview.blob}} |
||||
{{else}} |
||||
{{> avatar url=avatarPreview.blob}} |
||||
{{/if}} |
||||
{{else}} |
||||
{{> avatar username=user.username}} |
||||
{{/if}} |
||||
</div> |
||||
|
||||
<div class="rc-select-avatar__list"> |
||||
<div class="rc-select-avatar__list-item rc-tooltip js-select-avatar-initials" aria-label="{{_ "initials_avatar" }}"> |
||||
{{> avatar username=initialsUsername }} |
||||
</div> |
||||
<div class="rc-select-avatar__list-item rc-tooltip js-select-avatar-upload" aria-label="{{_ "Upload_user_avatar" }}"> |
||||
<label class="rc-select-avatar__upload-label avatar" for="upload-avatar"> |
||||
{{> icon block="rc-select-avatar__upload-icon" icon="upload"}} |
||||
</label> |
||||
<input type="file" name="" value="" id="upload-avatar" style="display:none;" accept="image/x-png,image/gif,image/jpeg"> |
||||
</div> |
||||
<div class="rc-select-avatar__list-item rc-tooltip js-select-avatar-url {{selectUrl}}" aria-label="{{_ "Use_url_for_avatar" }}"> |
||||
<label class="rc-select-avatar__upload-label avatar"> |
||||
{{> icon block="rc-select-avatar__upload-icon" icon="permalink"}} |
||||
</label> |
||||
</div> |
||||
|
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title"> |
||||
{{_ "Use_url_for_avatar"}} |
||||
</div> |
||||
<div class="rc-input__wrapper"> |
||||
<input name="avatar_url" class="rc-input__element js-avatar-url-input" placeholder="{{_ "Use_url_for_avatar"}}"> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="rc-input rc-form-group rc-form-group--small"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title"> |
||||
{{_ "Name"}} |
||||
</div> |
||||
<div class="rc-input__wrapper"> |
||||
<input name="name" type="text" class="rc-input__element rc-input__element--small" id="name" autocomplete="off" value="{{user.name}}"/> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<div class="rc-input rc-form-group rc-form-group--small"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title"> |
||||
{{_ "Username"}} |
||||
</div> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon icon='at' }} |
||||
</div> |
||||
<input name="name" type="text" class="rc-input__element rc-input__element--small" id="username" autocomplete="off" value="{{user.username}}"/> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<div class="rc-form-group rc-form-group--small"> |
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Email"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon icon='mail' }} |
||||
</div> |
||||
<input name="name" type="text" class="rc-input__element rc-input__element--small" id="email" autocomplete="off" value="{{user.emails.[0].address}}"/> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<div class="rc-switch"> |
||||
<label class="rc-switch__label" tabindex="-1"> |
||||
<input type="checkbox" class="rc-switch__input" name="type" id="verified" value="1" checked="{{user.emails.[0].verified}}"/> |
||||
<span class="rc-switch__button"> |
||||
<span class="rc-switch__button-inside"></span> |
||||
</span> |
||||
<span class="rc-switch__text"> |
||||
{{_ "Verified"}} |
||||
</span> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="rc-form-group rc-form-group--small"> |
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "StatusMessage"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon icon='edit' }} |
||||
</div> |
||||
<input name="name" type="text" maxlength="120" class="rc-input__element rc-input__element--small" id="status" autocomplete="off" value="{{user.statusText}}"/> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
|
||||
|
||||
<div class="rc-form-group rc-form-group--small"> |
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Bio"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon icon='edit' }} |
||||
</div> |
||||
<input name="name" type="text" maxlength="260" class="rc-input__element rc-input__element--small" id="bio" autocomplete="off" value="{{user.bio}}"/> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
|
||||
|
||||
{{#if hasPermission 'edit-other-user-password'}} |
||||
<div class="rc-form-group rc-form-group--small rc-form-group--inline" style="position: relative;"> |
||||
<div class="rc-input rc-form-item-inline"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Password"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon icon='key' }} |
||||
</div> |
||||
<input name="password" class="rc-input__element rc-input__element--small" type="password" id="password" autocomplete="off" value="" disabled="{{requirePasswordChangeDisabled}}" style="padding-right: 70px"/> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<button id="randomPassword" class="rc-button rc-button--primary rc-form-item-inline rc-button--nude rc-button--small" style="right: 0; position: absolute; bottom: 10px; " disabled="{{requirePasswordChangeDisabled}}">{{_ 'Random'}}</button> |
||||
</div> |
||||
|
||||
<div class="rc-form-group rc-form-group--small rc-switch"> |
||||
<label class="rc-switch__label" tabindex="-1"> |
||||
<input class="rc-switch__input" type="checkbox" id="setRandomPassword" value="1" checked="{{setRandomPassword}}"/> |
||||
<span class="rc-switch__button"> |
||||
<span class="rc-switch__button-inside"></span> |
||||
</span> |
||||
<span class="rc-switch__text"> |
||||
{{_ "Set_random_password_and_send_by_email"}} |
||||
</span> |
||||
</label> |
||||
</div> |
||||
<div class="rc-form-group rc-form-group--small rc-switch"> |
||||
<label class="rc-switch__label" tabindex="-1"> |
||||
<input class="rc-switch__input" type="checkbox" id="changePassword" value="1" checked="{{requirePasswordChange}}" disabled="{{requirePasswordChangeDisabled}}"/> |
||||
<span class="rc-switch__button"> |
||||
<span class="rc-switch__button-inside"></span> |
||||
</span> |
||||
<span class="rc-switch__text"> |
||||
{{_ "Require_password_change"}} |
||||
</span> |
||||
</label> |
||||
</div> |
||||
{{/if}} |
||||
|
||||
<div class="rc-form-group rc-form-group--small"> |
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__title">{{_ "Roles"}}</div> |
||||
<div class="rc-input__wrapper"> |
||||
{{#let roles=role}} |
||||
<select id="roleSelect" class="rc-input rc-form-item-inline rc-input--small" {{disabled role}}> |
||||
<option value="placeholder" disabled selected>{{_ "Select_role"}}</option> |
||||
{{#each roles}} |
||||
<option value="{{_id}}">{{name}}</option> |
||||
{{/each}} |
||||
</select> |
||||
{{/let}} |
||||
</div> |
||||
</label> |
||||
</div> |
||||
<ul id="roles" class="chip-container current-user-roles"> |
||||
{{#each userRoles}} |
||||
<li class="remove-role" title="{{this}}"><i class="icon icon-cancel-circled"></i>{{this}}</li> |
||||
{{/each}} |
||||
</ul> |
||||
</div> |
||||
|
||||
{{#unless user}} |
||||
<div class="input-line"> |
||||
<label for="joinDefaultChannels"> |
||||
<input type="checkbox" id="joinDefaultChannels" value="1" checked="true"> |
||||
{{_ "Join_default_channels"}} |
||||
</label> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="sendWelcomeEmail"> |
||||
<input type="checkbox" id="sendWelcomeEmail" value="1" checked="true"> |
||||
{{_ "Send_welcome_email"}} |
||||
</label> |
||||
</div> |
||||
{{/unless}} |
||||
<div class="rc-user-info__flex rc-user-info__row"> |
||||
<button class='rc-button rc-button--cancel cancel' type="button"><span>{{_ "Cancel"}}</span></button> |
||||
<button class='rc-button rc-button--primary save'><span>{{_ "Save"}}</span></button> |
||||
</div> |
||||
</form> |
||||
{{/unless}} |
||||
</section> |
||||
</template> |
||||
@ -1,316 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Random } from 'meteor/random'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import toastr from 'toastr'; |
||||
import s from 'underscore.string'; |
||||
|
||||
import { t, handleError } from '../../../utils'; |
||||
import { Roles } from '../../../models'; |
||||
import { Notifications } from '../../../notifications'; |
||||
import { hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { settings } from '../../../settings/client'; |
||||
import { callbacks } from '../../../callbacks/client'; |
||||
import { modal } from '../../../ui-utils/client'; |
||||
|
||||
Template.userEdit.helpers({ |
||||
|
||||
disabled(cursor) { |
||||
return cursor.count() === 0 ? 'disabled' : ''; |
||||
}, |
||||
canEditOrAdd() { |
||||
return (Template.instance().user && hasAtLeastOnePermission('edit-other-user-info')) || (!Template.instance().user && hasAtLeastOnePermission('create-user')); |
||||
}, |
||||
|
||||
selectUrl() { |
||||
return Template.instance().url.get().trim() ? '' : 'disabled'; |
||||
}, |
||||
|
||||
user() { |
||||
return Template.instance().user; |
||||
}, |
||||
|
||||
initialsUsername() { |
||||
const { user } = Template.instance(); |
||||
return `@${ user && user.username }`; |
||||
}, |
||||
|
||||
avatarPreview() { |
||||
return Template.instance().avatar.get(); |
||||
}, |
||||
|
||||
requirePasswordChange() { |
||||
return !Template.instance().user || Template.instance().user.requirePasswordChange; |
||||
}, |
||||
|
||||
requirePasswordChangeDisabled() { |
||||
// when setting a random password, requiring a password change is mandatory
|
||||
return Template.instance().setRandomPassword.get(); |
||||
}, |
||||
|
||||
setRandomPassword() { |
||||
return !Template.instance().user || Template.instance().user.setRandomPassword; |
||||
}, |
||||
|
||||
role() { |
||||
const roles = Template.instance().roles.get(); |
||||
return Roles.find({ _id: { $nin: roles }, scope: 'Users' }, { sort: { description: 1, _id: 1 } }); |
||||
}, |
||||
|
||||
userRoles() { |
||||
return Template.instance().roles.get(); |
||||
}, |
||||
|
||||
name() { |
||||
return this.description || this._id; |
||||
}, |
||||
}); |
||||
|
||||
Template.userEdit.events({ |
||||
'click .js-select-avatar-initials'(e, template) { |
||||
template.avatar.set({ |
||||
service: 'initials', |
||||
blob: `@${ template.user.username }`, |
||||
}); |
||||
}, |
||||
|
||||
'click .js-select-avatar-url'(e, template) { |
||||
const url = template.url.get().trim(); |
||||
if (!url) { |
||||
return; |
||||
} |
||||
|
||||
template.avatar.set({ |
||||
service: 'url', |
||||
contentType: '', |
||||
blob: url, |
||||
}); |
||||
}, |
||||
|
||||
'input .js-avatar-url-input'(e, template) { |
||||
const text = e.target.value; |
||||
template.url.set(text); |
||||
}, |
||||
|
||||
'change #setRandomPassword'(e, template) { |
||||
const requiring = e.currentTarget.checked; |
||||
template.setRandomPassword.set(requiring); |
||||
|
||||
if (requiring) { |
||||
$(e.currentTarget.form).find('#changePassword')[0].checked = true; |
||||
$(e.currentTarget.form).find('#password')[0].value = ''; |
||||
} |
||||
}, |
||||
|
||||
'click #randomPassword'(e) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
e.target.classList.add('loading'); |
||||
$('#password').val(''); |
||||
setTimeout(() => { |
||||
$('#password').val(Random.id()); |
||||
e.target.classList.remove('loading'); |
||||
}, 1000); |
||||
}, |
||||
|
||||
'change .js-select-avatar-upload [type=file]'(event, template) { |
||||
const e = event.originalEvent || event; |
||||
let { files } = e.target; |
||||
if (!files || files.length === 0) { |
||||
files = (e.dataTransfer && e.dataTransfer.files) || []; |
||||
} |
||||
Object.keys(files).forEach((key) => { |
||||
const blob = files[key]; |
||||
if (!/image\/.+/.test(blob.type)) { |
||||
return; |
||||
} |
||||
const reader = new FileReader(); |
||||
reader.readAsDataURL(blob); |
||||
reader.onloadend = function() { |
||||
template.avatar.set({ |
||||
service: 'upload', |
||||
contentType: blob.type, |
||||
blob: reader.result, |
||||
}); |
||||
}; |
||||
}); |
||||
}, |
||||
|
||||
'click .cancel'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.roles.set([]); |
||||
t.cancel(t.find('form')); |
||||
}, |
||||
|
||||
'click .remove-role'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
let roles = t.roles.get(); |
||||
roles = roles.filter((el) => el !== this.valueOf()); |
||||
t.roles.set(roles); |
||||
$(`[title=${ this }]`).remove(); |
||||
}, |
||||
|
||||
'mouseover #password'(e) { |
||||
e.target.type = 'text'; |
||||
}, |
||||
|
||||
'mouseout #password'(e) { |
||||
e.target.type = 'password'; |
||||
}, |
||||
|
||||
'change #roleSelect'(e, instance) { |
||||
const select = $('#roleSelect'); |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
if (select.find(':selected').is(':disabled')) { |
||||
return; |
||||
} |
||||
const userRoles = [...instance.roles.get()]; |
||||
userRoles.push(select.val()); |
||||
instance.roles.set(userRoles); |
||||
select.val('placeholder'); |
||||
}, |
||||
|
||||
'submit form'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.save(e.currentTarget); |
||||
}, |
||||
}); |
||||
|
||||
Template.userEdit.onCreated(function() { |
||||
this.user = this.data != null ? this.data.user : undefined; |
||||
this.roles = this.user ? new ReactiveVar(this.user.roles) : new ReactiveVar([]); |
||||
this.avatar = new ReactiveVar(); |
||||
this.url = new ReactiveVar(''); |
||||
this.setRandomPassword = new ReactiveVar(!this.user); |
||||
|
||||
Notifications.onLogged('updateAvatar', () => this.avatar.set()); |
||||
|
||||
const { tabBar } = Template.currentData(); |
||||
|
||||
this.cancel = (form, data) => { |
||||
form.reset(); |
||||
this.$('input[type=checkbox]').prop('checked', true); |
||||
if (this.user) { |
||||
return this.data.back(data); |
||||
} |
||||
return tabBar.close(); |
||||
}; |
||||
|
||||
this.getUserData = () => { |
||||
const userData = { _id: this.user != null ? this.user._id : undefined }; |
||||
userData.name = s.trim(this.$('#name').val()); |
||||
userData.username = s.trim(this.$('#username').val()); |
||||
userData.statusText = s.trim(this.$('#status').val()); |
||||
userData.bio = s.trim(this.$('#bio').val()); |
||||
userData.email = s.trim(this.$('#email').val()); |
||||
userData.verified = this.$('#verified:checked').length > 0; |
||||
userData.password = s.trim(this.$('#password').val()); |
||||
userData.setRandomPassword = this.$('#setRandomPassword:checked').length > 0; |
||||
userData.requirePasswordChange = this.$('#changePassword:checked').length > 0; |
||||
userData.joinDefaultChannels = this.$('#joinDefaultChannels:checked').length > 0; |
||||
userData.sendWelcomeEmail = this.$('#sendWelcomeEmail:checked').length > 0; |
||||
const roleSelect = this.$('.remove-role').toArray(); |
||||
|
||||
if (roleSelect.length > 0) { |
||||
const notSorted = roleSelect.map((role) => role.title); |
||||
// Remove duplicate strings from the array
|
||||
userData.roles = notSorted.filter((el, index) => notSorted.indexOf(el) === index); |
||||
} |
||||
return userData; |
||||
}; |
||||
|
||||
this.validate = () => { |
||||
const userData = this.getUserData(); |
||||
|
||||
const errors = []; |
||||
if (settings.get('Accounts_RequireNameForSignUp') && !userData.name) { |
||||
errors.push('Name'); |
||||
} |
||||
if (!userData.username) { |
||||
errors.push('Username'); |
||||
} |
||||
if (!userData.email) { |
||||
errors.push('Email'); |
||||
} |
||||
|
||||
if (!userData.roles) { |
||||
errors.push('Roles'); |
||||
} |
||||
|
||||
for (const error of Array.from(errors)) { |
||||
toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__(error) })); |
||||
} |
||||
|
||||
return errors.length === 0; |
||||
}; |
||||
|
||||
this.save = (form) => { |
||||
if (!this.validate()) { |
||||
return; |
||||
} |
||||
const userData = this.getUserData(); |
||||
if (this.user != null) { |
||||
for (const key in userData) { |
||||
if (key) { |
||||
const value = userData[key]; |
||||
if (!['_id'].includes(key)) { |
||||
if (value === this.user[key]) { |
||||
delete userData[key]; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
const avatar = this.avatar.get(); |
||||
if (avatar) { |
||||
let method; |
||||
const params = []; |
||||
|
||||
if (avatar.service === 'initials') { |
||||
method = 'resetAvatar'; |
||||
} else { |
||||
method = 'setAvatarFromService'; |
||||
params.push(avatar.blob, avatar.contentType, avatar.service); |
||||
} |
||||
|
||||
Meteor.call(method, ...params, Template.instance().user._id, function(err) { |
||||
if (err && err.details) { |
||||
toastr.error(t(err.message)); |
||||
} else { |
||||
toastr.success(t('Avatar_changed_successfully')); |
||||
callbacks.run('userAvatarSet', avatar.service); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
Meteor.call('insertOrUpdateUser', userData, (error) => { |
||||
if (error) { |
||||
if (error.error === 'error-max-guests-number-reached') { |
||||
const message = TAPi18n.__('You_reached_the_maximum_number_of_guest_users_allowed_by_your_license.'); |
||||
const url = 'https://go.rocket.chat/i/guest-limit-exceeded'; |
||||
const email = 'sales@rocket.chat'; |
||||
const linkText = TAPi18n.__('Click_here_for_more_details_or_contact_sales_for_a_new_license', { url, email }); |
||||
|
||||
modal.open({ |
||||
type: 'error', |
||||
title: TAPi18n.__('Maximum_number_of_guests_reached'), |
||||
text: `${ message } ${ linkText }`, |
||||
html: true, |
||||
}); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
return handleError(error); |
||||
} |
||||
toastr.success(userData._id ? t('User_updated_successfully') : t('User_added_successfully')); |
||||
this.cancel(form, userData); |
||||
}); |
||||
}; |
||||
}); |
||||
@ -1,128 +0,0 @@ |
||||
<template name="userInfo"> |
||||
{{#unless hideHeader}} |
||||
<header class="contextual-bar__header"> |
||||
<div class="contextual-bar__header-data"> |
||||
{{#if showBackButton}} |
||||
<button class="rc-button rc-button--nude contextual-bar__header-back-btn js-back" title="{{_ 'View_All'}}"> |
||||
<i class="icon-angle-left"></i> |
||||
</button> |
||||
{{/if}} |
||||
{{> icon icon="user" block="contextual-bar__header-icon"}} |
||||
<h1 class="contextual-bar__header-title">{{_ "User_Info"}}</h1> |
||||
</div> |
||||
<button class="contextual-bar__header-close js-close" aria-label="{{_ "Close"}}"> |
||||
{{> icon block="contextual-bar__header-close-icon" icon="plus"}} |
||||
</button> |
||||
</header> |
||||
{{/unless}} |
||||
|
||||
{{#if isLoading}} |
||||
{{> loading}} |
||||
{{else}} |
||||
{{#if editingUser}} |
||||
{{> userEdit (userToEdit)}} |
||||
{{else}} |
||||
{{#with user}} |
||||
<section class="contextual-bar__content"> |
||||
<div class="rc-user-info-wrapper"> |
||||
<div class="rc-user-info"> |
||||
<div class="rc-user-info__avatar"> |
||||
{{> avatar username=username}} |
||||
</div> |
||||
<h3 title="{{name}}" class="rc-user-info__name">{{name}}{{#if nickname}} ({{nickname}}){{/if}}</h3> |
||||
{{#if username}}<p class="rc-user-info__username">@{{username}}</p>{{/if}} |
||||
{{# userPresence uid=uid}}<span class="rc-header__status rc-user-info__status"> |
||||
<div class="rc-header__status-bullet rc-header__status-bullet--{{userStatus}}" title="{{_ userStatus}}"></div> |
||||
<div class="rc-header__visual-status">{{userStatusText}}</div> |
||||
</span>{{/userPresence}} |
||||
</div> |
||||
|
||||
<div class="rc-user-info-action"> |
||||
{{#each actions}} |
||||
<button class="js-action rc-user-info-action__item"> |
||||
{{> icon block="rc-user-info-action__icon" icon=icon }} |
||||
{{_ ./name}} |
||||
</button> |
||||
{{/each}} |
||||
{{# with moreActions}} |
||||
<button class="rc-tooltip rc-room-actions__button js-more" aria-label="{{_ 'More'}}"> |
||||
{{> icon block="tab-button-icon" icon="menu" }} |
||||
</button> |
||||
{{/with}} |
||||
</div> |
||||
|
||||
{{#if hasBio}} |
||||
<div class="rc-user-info-details"> |
||||
{{ bio }} |
||||
</div> |
||||
{{/if}} |
||||
<div class="rc-user-info-details"> |
||||
{{#if roleTags}} |
||||
<div class="rc-user-info-details__item"> |
||||
<label class="rc-user-info-details__label">{{_ "Roles"}}</label> |
||||
<ul class="chip-container current-user-roles"> |
||||
{{#each roleTags}} |
||||
<li class="role-tag" title="{{description}}">{{description}}</li> |
||||
{{/each}} |
||||
</ul> |
||||
</div> |
||||
{{/if}} |
||||
{{#if hasPermission 'view-full-other-user-info'}} |
||||
{{#if hasEmails}} |
||||
<div class="rc-user-info-details__item"> |
||||
<label class="rc-user-info-details__label">{{_ "Email"}}</label> |
||||
{{#each emails}} <a href="mailto:{{address}}" class="rc-user-info-details__info">{{address}}{{#if verified}} <i class="icon-ok success-color"></i>{{/if}}</a> {{/each}} |
||||
</div> |
||||
{{/if}} |
||||
{{#if hasPhone}} |
||||
<div class="rc-user-info-details__item"> |
||||
<label class="rc-user-info-details__label">{{_ "Phone_number"}}</label> |
||||
{{#each phone}} <a href="tel:{{phoneNumber}}" class="rc-user-info-details__info">{{phoneNumber}}</a> {{/each}} |
||||
</div> |
||||
{{/if}} |
||||
{{#if createdAt}} |
||||
<div class="rc-user-info-details__item"> |
||||
<label class="rc-user-info-details__label">{{_ "Created_at"}}</label> |
||||
<p class="rc-user-info-details__info">{{createdAt}}</p> |
||||
</div> |
||||
<div class="rc-user-info-details__item"> |
||||
<label class="rc-user-info-details__label">{{_ "Last_login"}}</label> |
||||
<p class="rc-user-info-details__info">{{lastLogin}}</p> |
||||
</div> |
||||
{{/if}} |
||||
{{#if shouldDisplayReason}} |
||||
<div class="rc-user-info-details__item"> |
||||
<label class="rc-user-info-details__label">{{_ "Reason_To_Join"}}</label> |
||||
<p class="rc-user-info-details__info">{{user.reason}}</p> |
||||
</div> |
||||
{{/if}} |
||||
{{/if}} |
||||
{{#if utc}} |
||||
<div class="rc-user-info-details__item"> |
||||
<label class="rc-user-info-details__label">{{_ "Timezone"}}</label> |
||||
<p class="rc-user-info-details__info">{{userTime}} (UTC {{utc}})</p> |
||||
</div> |
||||
{{/if}} |
||||
{{#each customField}} |
||||
<div class="rc-user-info-details__item"> |
||||
<label class="rc-user-info-details__label">{{_ label}}</label> |
||||
<p class="rc-user-info-details__info">{{value}}</p> |
||||
</div> |
||||
{{/each}} |
||||
<div class="rc-user-info-details__item"> |
||||
{{#if services.facebook.id}} <p class="secondary-font-color"><i class="icon-facebook"></i><a href="{{services.facebook.link}}" target="_blank">{{services.facebook.name}}</a></p> {{/if}} |
||||
{{#if services.github.id}} <p class="secondary-font-color"><i class="icon-github-circled"></i><a href="https://www.github.com/{{services.github.username}}" target="_blank">{{services.github.username}}</a></p> {{/if}} |
||||
{{#if services.gitlab.id}} <p class="secondary-font-color"><i class="icon-gitlab"></i>{{services.gitlab.username}}</p> {{/if}} |
||||
{{#if services.google.id}} <p class="secondary-font-color"><i class="icon-gplus"></i><a href="https://plus.google.com/{{services.google.id}}" target="_blank">{{services.google.name}}</a></p> {{/if}} |
||||
{{#if services.linkedin.id}} <p class="secondary-font-color"><i class="icon-linkedin"></i><a href="{{services.linkedin.publicProfileUrl}}" target="_blank">{{linkedinUsername}}</a></p> {{/if}} |
||||
{{#if servicesMeteor.id}} <p class="secondary-font-color"><i class="icon-meteor"></i>{{servicesMeteor.username}}</p> {{/if}} |
||||
{{#if services.twitter.id}} <p class="secondary-font-color"><i class="icon-twitter"></i><a href="https://twitter.com/{{services.twitter.screenName}}" target="_blank">{{services.twitter.screenName}}</a></p> {{/if}} |
||||
{{#if services.wordpress.id}} <p class="secondary-font-color"><i class="icon-wordpress"></i>{{services.wordpress.user_login}}</p> {{/if}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</section> |
||||
{{/with}} |
||||
{{/if}} |
||||
{{/if}} |
||||
</template> |
||||
@ -1,331 +0,0 @@ |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Session } from 'meteor/session'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import _ from 'underscore'; |
||||
import s from 'underscore.string'; |
||||
import moment from 'moment'; |
||||
|
||||
import { DateFormat } from '../../../lib'; |
||||
import { popover } from '../../../ui-utils'; |
||||
import { templateVarHandler } from '../../../utils'; |
||||
import { RoomRoles, UserRoles, Roles } from '../../../models'; |
||||
import { settings } from '../../../settings'; |
||||
import { getActions } from './userActions'; |
||||
import './userInfo.html'; |
||||
import { APIClient } from '../../../utils/client'; |
||||
import { Markdown } from '../../../markdown/lib/markdown'; |
||||
|
||||
const shownActionsCount = 2; |
||||
|
||||
const moreActions = function() { |
||||
return ( |
||||
Template.instance().actions.get() |
||||
.map((action) => (typeof action === 'function' ? action.call(this) : action)) |
||||
.filter((action) => action && (!action.condition || action.condition.call(this))) |
||||
.slice(shownActionsCount) |
||||
); |
||||
}; |
||||
|
||||
Template.userInfo.helpers({ |
||||
hideHeader() { |
||||
return ['Template.adminUserInfo', 'adminUserInfo'].includes(Template.parentData(2).viewName); |
||||
}, |
||||
|
||||
moreActions, |
||||
|
||||
actions() { |
||||
return Template.instance().actions.get() |
||||
.map((action) => (typeof action === 'function' ? action.call(this) : action)) |
||||
.filter((action) => action && (!action.condition || action.condition.call(this))) |
||||
.slice(0, shownActionsCount); |
||||
}, |
||||
|
||||
customField() { |
||||
const sCustomFieldsToShow = settings.get('Accounts_CustomFieldsToShowInUserInfo').trim(); |
||||
const customFields = []; |
||||
|
||||
if (sCustomFieldsToShow) { |
||||
const user = Template.instance().user.get(); |
||||
const userCustomFields = (user && user.customFields) || {}; |
||||
const listOfCustomFieldsToShow = JSON.parse(sCustomFieldsToShow); |
||||
|
||||
_.map(listOfCustomFieldsToShow, (el) => { |
||||
let content = ''; |
||||
if (_.isObject(el)) { |
||||
_.map(el, (key, label) => { |
||||
const value = templateVarHandler(key, userCustomFields); |
||||
if (value) { |
||||
content = { label, value }; |
||||
} |
||||
}); |
||||
} else { |
||||
content = templateVarHandler(el, userCustomFields); |
||||
} |
||||
if (content) { |
||||
customFields.push(content); |
||||
} |
||||
}); |
||||
} |
||||
return customFields; |
||||
}, |
||||
uid() { |
||||
const user = Template.instance().user.get(); |
||||
return user._id; |
||||
}, |
||||
name() { |
||||
const user = Template.instance().user.get(); |
||||
return user && user.name ? user.name : TAPi18n.__('Unnamed'); |
||||
}, |
||||
|
||||
username() { |
||||
const user = Template.instance().user.get(); |
||||
return user && user.username; |
||||
}, |
||||
|
||||
userStatus() { |
||||
const user = Template.instance().user.get(); |
||||
const userStatus = Session.get(`user_${ user.username }_status`); |
||||
return userStatus || TAPi18n.__('offline'); |
||||
}, |
||||
|
||||
userStatusText() { |
||||
if (s.trim(this.statusText)) { |
||||
return this.statusText; |
||||
} |
||||
|
||||
const user = Template.instance().user.get(); |
||||
const userStatus = Session.get(`user_${ user.username }_status`); |
||||
return userStatus || TAPi18n.__('offline'); |
||||
}, |
||||
|
||||
email() { |
||||
const user = Template.instance().user.get(); |
||||
return user && user.emails && user.emails[0] && user.emails[0].address; |
||||
}, |
||||
|
||||
utc() { |
||||
const user = Template.instance().user.get(); |
||||
if (user && user.utcOffset != null) { |
||||
if (user.utcOffset > 0) { |
||||
return `+${ user.utcOffset }`; |
||||
} |
||||
return user.utcOffset; |
||||
} |
||||
}, |
||||
|
||||
lastLogin() { |
||||
const user = Template.instance().user.get(); |
||||
if (user && user.lastLogin) { |
||||
return DateFormat.formatDateAndTime(user.lastLogin); |
||||
} |
||||
}, |
||||
|
||||
createdAt() { |
||||
const user = Template.instance().user.get(); |
||||
if (user && user.createdAt) { |
||||
return DateFormat.formatDateAndTime(user.createdAt); |
||||
} |
||||
}, |
||||
linkedinUsername() { |
||||
const user = Template.instance().user.get(); |
||||
if (user && user.services && user.services.linkedin && user.services.linkedin.publicProfileUrl) { |
||||
return s.strRight(user.services.linkedin.publicProfileUrl, '/in/'); |
||||
} |
||||
}, |
||||
|
||||
servicesMeteor() { |
||||
const user = Template.instance().user.get(); |
||||
return user && user.services && user.services['meteor-developer']; |
||||
}, |
||||
|
||||
userTime() { |
||||
const user = Template.instance().user.get(); |
||||
if (user && user.utcOffset != null) { |
||||
return DateFormat.formatTime(Template.instance().now.get().utcOffset(user.utcOffset)); |
||||
} |
||||
}, |
||||
|
||||
user() { |
||||
return Template.instance().user.get(); |
||||
}, |
||||
|
||||
hasEmails() { |
||||
return _.isArray(this.emails); |
||||
}, |
||||
|
||||
hasPhone() { |
||||
return _.isArray(this.phone); |
||||
}, |
||||
|
||||
isLoading() { |
||||
return Template.instance().loadingUserInfo.get(); |
||||
}, |
||||
|
||||
editingUser() { |
||||
return Template.instance().editingUser.get(); |
||||
}, |
||||
|
||||
userToEdit() { |
||||
const instance = Template.instance(); |
||||
const data = Template.currentData(); |
||||
return { |
||||
user: instance.user.get(), |
||||
back({ _id, username }) { |
||||
instance.editingUser.set(); |
||||
|
||||
if (_id) { |
||||
data.onChange && data.onChange(); |
||||
return instance.loadUser({ _id }); |
||||
} |
||||
|
||||
if (username != null) { |
||||
const user = instance.user.get(); |
||||
if ((user != null ? user.username : undefined) !== username) { |
||||
data.username = username; |
||||
return instance.loadedUsername.set(username); |
||||
} |
||||
} |
||||
}, |
||||
}; |
||||
}, |
||||
hasBio() { |
||||
const user = Template.instance().user.get(); |
||||
return user.bio && user.bio.trim(); |
||||
}, |
||||
nickname() { |
||||
const user = Template.instance().user.get(); |
||||
return user.nickname?.trim(); |
||||
}, |
||||
bio() { |
||||
const user = Template.instance().user.get(); |
||||
return Markdown.parse(user.bio); |
||||
}, |
||||
roleTags() { |
||||
const user = Template.instance().user.get(); |
||||
if (!user || !user._id) { |
||||
return; |
||||
} |
||||
const userRoles = UserRoles.findOne(user._id) || {}; |
||||
const roomRoles = RoomRoles.findOne({ 'u._id': user._id, rid: Session.get('openedRoom') }) || {}; |
||||
const roles = _.union(userRoles.roles || [], roomRoles.roles || []); |
||||
return roles.length && Roles.find({ _id: { $in: roles }, description: { $exists: 1 } }, { fields: { description: 1 } }); |
||||
}, |
||||
|
||||
shouldDisplayReason() { |
||||
const user = Template.instance().user.get(); |
||||
return settings.get('Accounts_ManuallyApproveNewUsers') && user.active === false && user.reason; |
||||
}, |
||||
}); |
||||
|
||||
Template.userInfo.events({ |
||||
'click .js-more'(e, instance) { |
||||
const actions = moreActions.call(this); |
||||
const groups = []; |
||||
const columns = []; |
||||
const admin = actions.filter((actions) => actions.group === 'admin'); |
||||
const others = actions.filter((action) => !action.group); |
||||
const channel = actions.filter((actions) => actions.group === 'channel'); |
||||
if (others.length) { |
||||
groups.push({ items: others }); |
||||
} |
||||
if (channel.length) { |
||||
groups.push({ items: channel }); |
||||
} |
||||
|
||||
if (admin.length) { |
||||
groups.push({ items: admin }); |
||||
} |
||||
columns[0] = { groups }; |
||||
|
||||
$(e.currentTarget).blur(); |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
const config = { |
||||
columns, |
||||
data: { |
||||
rid: this._id, |
||||
username: instance.data.username, |
||||
instance, |
||||
}, |
||||
currentTarget: e.currentTarget, |
||||
offsetVertical: e.currentTarget.clientHeight + 10, |
||||
}; |
||||
popover.open(config); |
||||
}, |
||||
'click .js-action'(e) { |
||||
return this.action && this.action.apply(this, [e, { instance: Template.instance() }]); |
||||
}, |
||||
'click .js-close-info'(e, instance) { |
||||
return instance.clear(); |
||||
}, |
||||
'click .js-close'(e, instance) { |
||||
return instance.clear(); |
||||
}, |
||||
|
||||
'click .js-back'(e, instance) { |
||||
return instance.clear(); |
||||
}, |
||||
}); |
||||
|
||||
Template.userInfo.onCreated(function() { |
||||
this.now = new ReactiveVar(moment()); |
||||
this.user = new ReactiveVar(); |
||||
this.actions = new ReactiveVar(); |
||||
|
||||
this.autorun(() => { |
||||
const user = this.user.get(); |
||||
if (!user) { |
||||
this.actions.set([]); |
||||
return; |
||||
} |
||||
const actions = getActions({ |
||||
user, |
||||
hideAdminControls: this.data.hideAdminControls, |
||||
directActions: this.data.showAll, |
||||
}); |
||||
this.actions.set(actions); |
||||
}); |
||||
this.editingUser = new ReactiveVar(); |
||||
this.loadingUserInfo = new ReactiveVar(true); |
||||
this.tabBar = Template.currentData().tabBar; |
||||
this.nowInterval = setInterval(() => this.now.set(moment()), 30000); |
||||
|
||||
this.loadUser = async ({ _id, username }) => { |
||||
this.loadingUserInfo.set(true); |
||||
|
||||
const params = {}; |
||||
|
||||
if (_id != null) { |
||||
params.userId = _id; |
||||
} else if (username != null) { |
||||
params.username = username; |
||||
} else { |
||||
return; |
||||
} |
||||
|
||||
const { user } = await APIClient.v1.get('users.info', params); |
||||
this.user.set(user); |
||||
this.loadingUserInfo.set(false); |
||||
}; |
||||
|
||||
this.autorun(async () => { |
||||
const data = Template.currentData(); |
||||
if (!data) { |
||||
return; |
||||
} |
||||
|
||||
this.loadUser(data); |
||||
}); |
||||
|
||||
this.autorun(() => { |
||||
const data = Template.currentData(); |
||||
if (data.clear != null) { |
||||
this.clear = data.clear; |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
Template.userInfo.onDestroyed(function() { |
||||
clearInterval(this.nowInterval); |
||||
}); |
||||
@ -0,0 +1,38 @@ |
||||
import { Tracker } from 'meteor/tracker'; |
||||
|
||||
import { createEphemeralPortal } from '../../../../client/reactAdapters'; |
||||
|
||||
const Dep = new Tracker.Dependency(); |
||||
let state; |
||||
let dom; |
||||
let unregister; |
||||
const createAchor = () => { |
||||
const div = document.createElement('div'); |
||||
div.id = 'react-user-card'; |
||||
document.body.appendChild(div); |
||||
return div; |
||||
}; |
||||
|
||||
|
||||
export const closeUserCard = () => { |
||||
if (!dom) { |
||||
return; |
||||
} |
||||
unregister(); |
||||
unregister = undefined; |
||||
}; |
||||
|
||||
const props = () => { |
||||
Dep.depend(); |
||||
return state; |
||||
}; |
||||
|
||||
export const openUserCard = async ({ ...args }) => { |
||||
dom = dom || createAchor(); |
||||
state = { |
||||
onClose: closeUserCard, |
||||
...args, |
||||
}; |
||||
Dep.changed(); |
||||
unregister = unregister || await createEphemeralPortal(() => import('../../../../client/channel/UserCard'), props, dom); |
||||
}; |
||||
@ -1,6 +0,0 @@ |
||||
import { useCallback } from 'react'; |
||||
|
||||
import { useReactiveValue } from '../../hooks/useReactiveValue'; |
||||
import { Subscriptions } from '../../../app/models/client'; |
||||
|
||||
export const useUserSubscription = (rid, fields) => useReactiveValue(useCallback(() => Subscriptions.findOne({ rid }, { fields }), [rid, fields])); |
||||
@ -0,0 +1,90 @@ |
||||
import React, { useMemo, useRef, useCallback } from 'react'; |
||||
import { PositionAnimated, AnimatedVisibility, Menu, Option } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; |
||||
import { useSetting } from '../../contexts/SettingsContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import UserCard from '../../components/basic/UserCard'; |
||||
import { Backdrop } from '../../components/basic/Backdrop'; |
||||
import * as UserStatus from '../../components/basic/UserStatus'; |
||||
import { LocalTime } from '../../components/basic/UTCClock'; |
||||
import { useUserInfoActions, useUserInfoActionsSpread } from '../hooks/useUserInfoActions'; |
||||
|
||||
const UserCardWithData = ({ username, onClose, target, open, rid }) => { |
||||
const ref = useRef(target); |
||||
|
||||
const t = useTranslation(); |
||||
|
||||
const showRealNames = useSetting('UI_Use_Real_Name'); |
||||
|
||||
const query = useMemo(() => ({ username }), [username]); |
||||
|
||||
const { data, state } = useEndpointDataExperimental('users.info', query); |
||||
|
||||
ref.current = target; |
||||
|
||||
const user = useMemo(() => { |
||||
const loading = state === ENDPOINT_STATES.LOADING; |
||||
const defaultValue = loading ? undefined : null; |
||||
|
||||
const { user } = data || { user: {} }; |
||||
|
||||
const { |
||||
_id, |
||||
name = username, |
||||
roles = defaultValue, |
||||
status, |
||||
statusText = status, |
||||
bio = defaultValue, |
||||
utcOffset = defaultValue, |
||||
} = user; |
||||
|
||||
return { |
||||
_id, |
||||
name: showRealNames ? name : username, |
||||
username, |
||||
roles: roles && roles.map((role, index) => ( |
||||
<UserCard.Role key={index}>{role}</UserCard.Role> |
||||
)), |
||||
bio, |
||||
localTime: Number.isInteger(utcOffset) && ( |
||||
<LocalTime utcOffset={utcOffset} /> |
||||
), |
||||
status: UserStatus.getStatus(status), |
||||
customStatus: statusText, |
||||
}; |
||||
}, [data, username, showRealNames, state]); |
||||
|
||||
const handleOpen = useCallback( |
||||
(e) => { |
||||
open && open(e); |
||||
onClose && onClose(); |
||||
}, |
||||
[open, onClose], |
||||
); |
||||
|
||||
const { actions: actionsDefinition, menu: menuOptions } = useUserInfoActionsSpread(useUserInfoActions(user, rid)); |
||||
|
||||
const menu = menuOptions && <Menu flexShrink={0} mi='x2' key='menu' ghost={false} renderItem={({ label: { label, icon }, ...props }) => <Option {...props} label={label} icon={icon} />} options={menuOptions}/>; |
||||
|
||||
const actions = useMemo(() => [...actionsDefinition.map(([key, { label, icon, action }]) => <UserCard.Action key={key} title={label} aria-label={label} onClick={action} icon={icon}/>), menu].filter(Boolean), [actionsDefinition, menu]); |
||||
|
||||
return (<> |
||||
<Backdrop bg='transparent' onClick={onClose}/> |
||||
<PositionAnimated |
||||
anchor={ref} |
||||
placement='top-start' |
||||
margin={8} |
||||
visible={AnimatedVisibility.UNHIDING} |
||||
> |
||||
<UserCard |
||||
{...user} |
||||
onClose={onClose} |
||||
open={handleOpen} |
||||
actions={actions} |
||||
t={t}/> |
||||
</PositionAnimated></> |
||||
); |
||||
}; |
||||
|
||||
export default UserCardWithData; |
||||
@ -0,0 +1,19 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { ButtonGroup, Menu, Option } from '@rocket.chat/fuselage'; |
||||
|
||||
import UserInfo from '../../../components/basic/UserInfo'; |
||||
import { useUserInfoActions, useUserInfoActionsSpread } from '../../hooks/useUserInfoActions'; |
||||
|
||||
const UserActions = ({ user, rid }) => { |
||||
const { actions: actionsDefinition, menu: menuOptions } = useUserInfoActionsSpread(useUserInfoActions(user, rid)); |
||||
|
||||
const menu = menuOptions && <Menu mi='x4' ghost={false} small={false} renderItem={({ label: { label, icon }, ...props }) => <Option {...props} label={label} icon={icon} />} flexShrink={0} key='menu' options={menuOptions} />; |
||||
|
||||
const actions = useMemo(() => [...actionsDefinition.map(([key, { label, icon, action }]) => <UserInfo.Action key={key} title={label} label={label} onClick={action} icon={icon}/>), menu].filter(Boolean), [actionsDefinition, menu]); |
||||
|
||||
return <ButtonGroup mi='neg-x4' flexShrink={0} flexWrap='nowrap' withTruncatedText justifyContent='center' flexShrink={0}> |
||||
{actions} |
||||
</ButtonGroup>; |
||||
}; |
||||
|
||||
export default UserActions; |
||||
@ -0,0 +1,88 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
|
||||
import { UserInfo } from '../../components/basic/UserInfo'; |
||||
import { |
||||
useEndpointDataExperimental, |
||||
ENDPOINT_STATES, |
||||
} from '../../hooks/useEndpointDataExperimental'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useSetting } from '../../contexts/SettingsContext'; |
||||
import * as UserStatus from '../../components/basic/UserStatus'; |
||||
import UserCard from '../../components/basic/UserCard'; |
||||
import { FormSkeleton } from '../../admin/users/Skeleton'; |
||||
import VerticalBar from '../../components/basic/VerticalBar'; |
||||
import UserActions from './actions/UserActions'; |
||||
|
||||
export const UserInfoWithData = React.memo(function UserInfoWithData({ uid, username, tabBar, rid, onClose, video, showBackButton, ...props }) { |
||||
const t = useTranslation(); |
||||
|
||||
const showRealNames = useSetting('UI_Use_Real_Name'); |
||||
|
||||
const { data, state, error } = useEndpointDataExperimental( |
||||
'users.info', |
||||
useMemo( |
||||
() => ({ ...uid && { userId: uid }, ...username && { username } }), |
||||
[uid, username], |
||||
), |
||||
); |
||||
|
||||
const user = useMemo(() => { |
||||
const { user } = data || { user: {} }; |
||||
const { |
||||
name, |
||||
username, |
||||
roles = [], |
||||
status, |
||||
statusText, |
||||
bio, |
||||
utcOffset, |
||||
lastLogin, |
||||
} = user; |
||||
return { |
||||
name: showRealNames ? name : username, |
||||
username, |
||||
lastLogin, |
||||
roles: roles.map((role, index) => ( |
||||
<UserCard.Role key={index}>{role}</UserCard.Role> |
||||
)), |
||||
bio, |
||||
phone: user.phone, |
||||
customFields: user.customFields, |
||||
email: user.emails?.find(({ address }) => !!address), |
||||
utcOffset, |
||||
createdAt: user.createdAt, |
||||
// localTime: <LocalTime offset={utcOffset} />,
|
||||
status: UserStatus.getStatus(status), |
||||
customStatus: statusText, |
||||
}; |
||||
}, [data, showRealNames]); |
||||
|
||||
return ( |
||||
<VerticalBar> |
||||
<VerticalBar.Header> |
||||
{t('User_Info')} |
||||
{onClose && <VerticalBar.Close onClick={onClose} />} |
||||
</VerticalBar.Header> |
||||
|
||||
{ |
||||
(error && <VerticalBar.Content> |
||||
<Box mbs='x16'>{t('User_not_found')}</Box> |
||||
</VerticalBar.Content>) |
||||
|| (state === ENDPOINT_STATES.LOADING && <VerticalBar.Content> |
||||
<FormSkeleton /> |
||||
</VerticalBar.Content>) |
||||
|| <UserInfo |
||||
{...user} |
||||
data={data.user} |
||||
// onChange={onChange}
|
||||
actions={<UserActions user={data.user} rid={rid}/>} |
||||
{...props} |
||||
p='x24' |
||||
/> |
||||
} |
||||
</VerticalBar> |
||||
); |
||||
}); |
||||
|
||||
export default UserInfoWithData; |
||||
@ -0,0 +1,326 @@ |
||||
import React, { useCallback, useMemo } from 'react'; |
||||
import { Button, ButtonGroup, Icon, Modal, Box } from '@rocket.chat/fuselage'; |
||||
import { useAutoFocus, useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useReactiveValue } from '../../hooks/useReactiveValue'; |
||||
import { usePermission, useAllPermissions } from '../../contexts/AuthorizationContext'; |
||||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; |
||||
import { useUserId } from '../../contexts/UserContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { WebRTC } from '../../../app/webrtc/client'; |
||||
import { useRoute } from '../../contexts/RouterContext'; |
||||
import { useSetModal } from '../../contexts/ModalContext'; |
||||
import { RoomRoles } from '../../../app/models/client'; |
||||
import { roomTypes, RoomMemberActions } from '../../../app/utils'; |
||||
import { useEndpointActionExperimental } from '../../hooks/useEndpointAction'; |
||||
import { useUserRoom } from './useUserRoom'; |
||||
import { useUserSubscription, useUserSubscriptionByName } from '../../contexts/SubscriptionContext'; |
||||
|
||||
|
||||
const useUserHasRoomRole = (uid, rid, role) => useReactiveValue(useCallback(() => !!RoomRoles.findOne({ rid, 'u._id': uid, roles: role }), [uid, rid, role])); |
||||
|
||||
const getShouldOpenDirectMessage = (currentSubscription, usernameSubscription, canOpenDirectMessage, username) => { |
||||
const canOpenDm = canOpenDirectMessage || usernameSubscription; |
||||
const directMessageIsNotAlreadyOpen = currentSubscription && currentSubscription.name !== username; |
||||
return canOpenDm && directMessageIsNotAlreadyOpen; |
||||
}; |
||||
|
||||
const getShouldAllowCalls = (webRTCInstance) => { |
||||
if (!webRTCInstance) { |
||||
return false; |
||||
} |
||||
|
||||
const { localUrl, remoteItems } = webRTCInstance; |
||||
const r = remoteItems.get() || []; |
||||
if (localUrl.get() === null && r.length === 0) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
}; |
||||
|
||||
const getUserIsMuted = (room, user, userCanPostReadonly) => { |
||||
if (room && room.ro) { |
||||
if (Array.isArray(room.unmuted) && room.unmuted.indexOf(user && user.username) !== -1) { |
||||
return false; |
||||
} |
||||
|
||||
if (userCanPostReadonly) { |
||||
return Array.isArray(room.muted) && (room.muted.indexOf(user && user.username) !== -1); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
return room && Array.isArray(room.muted) && room.muted.indexOf(user && user.username) > -1; |
||||
}; |
||||
|
||||
const WarningModal = ({ text, confirmText, close, confirm, ...props }) => { |
||||
const refAutoFocus = useAutoFocus(true); |
||||
const t = useTranslation(); |
||||
return <Modal {...props}> |
||||
<Modal.Header> |
||||
<Icon color='warning' name='modal-warning' size={20}/> |
||||
<Modal.Title>{t('Are_you_sure')}</Modal.Title> |
||||
<Modal.Close onClick={close}/> |
||||
</Modal.Header> |
||||
<Modal.Content fontScale='p1'> |
||||
{text} |
||||
</Modal.Content> |
||||
<Modal.Footer> |
||||
<ButtonGroup align='end'> |
||||
<Button ghost onClick={close}>{t('Cancel')}</Button> |
||||
<Button ref={refAutoFocus} primary danger onClick={confirm}>{confirmText}</Button> |
||||
</ButtonGroup> |
||||
</Modal.Footer> |
||||
</Modal>; |
||||
}; |
||||
|
||||
|
||||
const mapOptions = ([key, { action, label, icon }]) => [ |
||||
key, |
||||
{ |
||||
label: { label, icon }, // TODO fuselage
|
||||
action, |
||||
}, |
||||
]; |
||||
|
||||
export const useUserInfoActionsSpread = (actions, size = 2) => useMemo(() => { |
||||
const entries = Object.entries(actions); |
||||
|
||||
const options = entries.slice(0, size); |
||||
const menuOptions = entries.slice(size, entries.length).map(mapOptions); |
||||
const menu = menuOptions.length && Object.fromEntries(entries.slice(size, entries.length).map(mapOptions)); |
||||
|
||||
return { actions: options, menu }; |
||||
}, [actions, size]); |
||||
|
||||
export const useUserInfoActions = (user = {}, rid) => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const directRoute = useRoute('direct'); |
||||
|
||||
const setModal = useSetModal(); |
||||
|
||||
const { _id: uid } = user; |
||||
const ownUserId = useUserId(); |
||||
|
||||
const closeModal = useMutableCallback(() => setModal(null)); |
||||
|
||||
const room = useUserRoom(rid); |
||||
const currentSubscription = useUserSubscription(rid); |
||||
const usernameSubscription = useUserSubscriptionByName(user.username); |
||||
|
||||
const isLeader = useUserHasRoomRole(uid, rid, 'leader'); |
||||
const isModerator = useUserHasRoomRole(uid, rid, 'moderator'); |
||||
const isOwner = useUserHasRoomRole(uid, rid, 'owner'); |
||||
|
||||
const getWebRTCInstance = useCallback(() => WebRTC.getInstanceByRoomId(rid), [rid]); |
||||
const webRTCInstance = useReactiveValue(getWebRTCInstance); |
||||
|
||||
const otherUserCanPostReadonly = useAllPermissions('post-readonly', rid); |
||||
|
||||
const isIgnored = currentSubscription && currentSubscription.ignored && currentSubscription.ignored.indexOf(uid) > -1; |
||||
const isMuted = getUserIsMuted(room, user, otherUserCanPostReadonly); |
||||
|
||||
const endpointPrefix = room.t === 'p' ? 'groups' : 'channels'; |
||||
|
||||
const roomConfig = room && room.t && roomTypes.getConfig(room.t); |
||||
|
||||
const { |
||||
roomCanSetOwner, |
||||
roomCanSetLeader, |
||||
roomCanSetModerator, |
||||
roomCanBlock, |
||||
roomCanMute, |
||||
roomCanRemove, |
||||
} = { |
||||
...roomConfig && { |
||||
roomCanSetOwner: roomConfig.allowMemberAction(room, RoomMemberActions.SET_AS_OWNER), |
||||
roomCanSetLeader: roomConfig.allowMemberAction(room, RoomMemberActions.SET_AS_LEADER), |
||||
roomCanSetModerator: roomConfig.allowMemberAction(room, RoomMemberActions.SET_AS_MODERATOR), |
||||
roomCanBlock: roomConfig.allowMemberAction(room, RoomMemberActions.IGNORE), |
||||
roomCanMute: roomConfig.allowMemberAction(room, RoomMemberActions.MUTE), |
||||
roomCanRemove: roomConfig.allowMemberAction(room, RoomMemberActions.REMOVE_USER), |
||||
}, |
||||
}; |
||||
|
||||
const roomName = room && room.t && roomTypes.getRoomName(room.t, room); |
||||
|
||||
const userCanSetOwner = usePermission('set-owner', rid); |
||||
const userCanSetLeader = usePermission('set-leader', rid); |
||||
const userCanSetModerator = usePermission('set-moderator', rid); |
||||
const userCanMute = usePermission('mute-user', rid); |
||||
const userCanRemove = usePermission('remove-user', rid); |
||||
const userCanDirectMessage = usePermission('create-d'); |
||||
|
||||
const shouldAllowCalls = getShouldAllowCalls(webRTCInstance); |
||||
const callInProgress = useReactiveValue(useCallback(() => webRTCInstance?.callInProgress?.get(), [])); |
||||
const shouldOpenDirectMessage = getShouldOpenDirectMessage(currentSubscription, usernameSubscription, userCanDirectMessage, user.username); |
||||
|
||||
const openDirectDm = useMutableCallback(() => directRoute.push({ |
||||
rid: user.username, |
||||
})); |
||||
|
||||
const openDirectMessageOption = useMemo(() => shouldOpenDirectMessage && { |
||||
label: t('Direct_Message'), |
||||
icon: 'chat', |
||||
action: openDirectDm, |
||||
}, [openDirectDm, shouldOpenDirectMessage, t]); |
||||
|
||||
const videoCallOption = useMemo(() => { |
||||
const joinCall = () => { |
||||
webRTCInstance.joinCall({ audio: true, video: true }); |
||||
}; |
||||
const startCall = () => { |
||||
webRTCInstance.startCall({ audio: true, video: true }); |
||||
}; |
||||
const action = callInProgress ? joinCall : startCall; |
||||
|
||||
return shouldAllowCalls && { |
||||
label: t(callInProgress ? 'Join_video_call' : 'Start_video_call'), |
||||
icon: 'video', |
||||
action, |
||||
}; |
||||
}, [callInProgress, shouldAllowCalls, t, webRTCInstance]); |
||||
|
||||
const audioCallOption = useMemo(() => { |
||||
const joinCall = () => { |
||||
webRTCInstance.joinCall({ audio: true, video: false }); |
||||
}; |
||||
const startCall = () => { |
||||
webRTCInstance.startCall({ audio: true, video: false }); |
||||
}; |
||||
const action = callInProgress ? joinCall : startCall; |
||||
|
||||
return shouldAllowCalls && { |
||||
label: t(callInProgress ? 'Join_audio_call' : 'Start_audio_call'), |
||||
icon: 'mic', |
||||
action, |
||||
}; |
||||
}, [callInProgress, shouldAllowCalls, t, webRTCInstance]); |
||||
|
||||
const changeOwnerEndpoint = isOwner ? 'removeOwner' : 'addOwner'; |
||||
const changeOwnerMessage = isOwner ? 'User__username__removed_from__room_name__owners' : 'User__username__is_now_a_owner_of__room_name_'; |
||||
const changeOwner = useEndpointActionExperimental('POST', `${ endpointPrefix }.${ changeOwnerEndpoint }`, t(changeOwnerMessage, { username: user.username, room_name: roomName })); |
||||
const changeOwnerAction = useMutableCallback(async () => changeOwner({ roomId: rid, userId: uid })); |
||||
const changeOwnerOption = useMemo(() => roomCanSetOwner && userCanSetOwner && { |
||||
label: t(isOwner ? 'Remove_as_owner' : 'Set_as_owner'), |
||||
icon: 'shield-check', |
||||
action: changeOwnerAction, |
||||
}, [changeOwnerAction, isOwner, t, roomCanSetOwner, userCanSetOwner]); |
||||
|
||||
const changeLeaderEndpoint = isLeader ? 'removeLeader' : 'addLeader'; |
||||
const changeLeaderMessage = isLeader ? 'User__username__removed_from__room_name__leaders' : 'User__username__is_now_a_leader_of__room_name_'; |
||||
const changeLeader = useEndpointActionExperimental('POST', `${ endpointPrefix }.${ changeLeaderEndpoint }`, t(changeLeaderMessage, { username: user.username, room_name: roomName })); |
||||
const changeLeaderAction = useMutableCallback(() => changeLeader({ roomId: rid, userId: uid })); |
||||
const changeLeaderOption = useMemo(() => roomCanSetLeader && userCanSetLeader && { |
||||
label: t(isLeader ? 'Remove_as_leader' : 'Set_as_leader'), |
||||
icon: 'shield-alt', |
||||
action: changeLeaderAction, |
||||
}, [isLeader, roomCanSetLeader, t, userCanSetLeader, changeLeaderAction]); |
||||
|
||||
const changeModeratorEndpoint = isModerator ? 'removeModerator' : 'addModerator'; |
||||
const changeModeratorMessage = isModerator ? 'User__username__removed_from__room_name__moderators' : 'User__username__is_now_a_moderator_of__room_name_'; |
||||
const changeModerator = useEndpointActionExperimental('POST', `${ endpointPrefix }.${ changeModeratorEndpoint }`, t(changeModeratorMessage, { username: user.username, room_name: roomName })); |
||||
const changeModeratorAction = useMutableCallback(() => changeModerator({ roomId: rid, userId: uid })); |
||||
const changeModeratorOption = useMemo(() => roomCanSetModerator && userCanSetModerator && { |
||||
label: t(isModerator ? 'Remove_as_moderator' : 'Set_as_moderator'), |
||||
icon: 'shield', |
||||
action: changeModeratorAction, |
||||
}, [changeModeratorAction, isModerator, roomCanSetModerator, t, userCanSetModerator]); |
||||
|
||||
const ignoreUser = useMethod('ignoreUser'); |
||||
const ignoreUserAction = useMutableCallback(async () => { |
||||
try { |
||||
await ignoreUser({ rid, ignoredUser: uid, ignore: !isIgnored }); |
||||
dispatchToastMessage({ type: 'success', message: t('User_has_been_unignored') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
const ignoreUserOption = useMemo(() => roomCanBlock && uid !== ownUserId && { |
||||
label: t(isIgnored ? 'Unignore' : 'Ignore'), |
||||
icon: 'ban', |
||||
action: ignoreUserAction, |
||||
}, [ignoreUserAction, isIgnored, ownUserId, roomCanBlock, t, uid]); |
||||
|
||||
const muteFn = useMethod(isMuted ? 'unmuteUserInRoom' : 'muteUserInRoom'); |
||||
const muteUserOption = useMemo(() => { |
||||
const action = () => { |
||||
const onConfirm = async () => { |
||||
try { |
||||
await muteFn({ rid, username: user.username }); |
||||
closeModal(); |
||||
dispatchToastMessage({ |
||||
type: 'success', |
||||
message: t( |
||||
isMuted |
||||
? 'User__username__unmuted_in_room__roomName__' |
||||
: 'User__username__muted_in_room__roomName__', |
||||
{ username: user.username, roomName }, |
||||
), |
||||
}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}; |
||||
|
||||
if (isMuted) { |
||||
return onConfirm(); |
||||
} |
||||
|
||||
setModal(<WarningModal |
||||
text={t('The_user_wont_be_able_to_type_in_s', roomName)} |
||||
close={closeModal} |
||||
confirmText={t('Yes_mute_user')} |
||||
confirm={onConfirm} |
||||
/>); |
||||
}; |
||||
|
||||
return roomCanMute && userCanMute && { |
||||
label: t(isMuted ? 'Unmute_user' : 'Mute_user'), |
||||
icon: isMuted ? 'mic' : 'mic-off', |
||||
action, |
||||
}; |
||||
}, [closeModal, dispatchToastMessage, isMuted, muteFn, rid, roomCanMute, roomName, setModal, t, user.username, userCanMute]); |
||||
|
||||
const removeUserAction = useEndpointActionExperimental('POST', `${ endpointPrefix }.kick`, t('User_has_been_removed_from_s', roomName)); |
||||
const removeUserOptionAction = useMutableCallback(() => { |
||||
setModal(<WarningModal |
||||
text={t('The_user_will_be_removed_from_s', roomName)} |
||||
close={closeModal} |
||||
confirmText={t('Yes_remove_user')} |
||||
confirm={() => { removeUserAction({ roomId: rid, userId: uid }); closeModal(); }} |
||||
/>); |
||||
}); |
||||
const removeUserOption = useMemo(() => roomCanRemove && userCanRemove && { |
||||
label: <Box color='danger'>{t('Remove_from_room')}</Box>, |
||||
icon: 'sign-out', |
||||
action: removeUserOptionAction, |
||||
}, [roomCanRemove, userCanRemove, removeUserOptionAction, t]); |
||||
|
||||
return useMemo(() => ({ |
||||
...openDirectMessageOption && { openDirectMessage: openDirectMessageOption }, |
||||
...videoCallOption && { video: videoCallOption }, |
||||
...audioCallOption && { audio: audioCallOption }, |
||||
...changeOwnerOption && { changeOwner: changeOwnerOption }, |
||||
...changeLeaderOption && { changeLeader: changeLeaderOption }, |
||||
...changeModeratorOption && { changeModerator: changeModeratorOption }, |
||||
...ignoreUserOption && { ignoreUser: ignoreUserOption }, |
||||
...muteUserOption && { muteUser: muteUserOption }, |
||||
...removeUserOption && { removeUser: removeUserOption }, |
||||
}), |
||||
[ |
||||
audioCallOption, |
||||
changeLeaderOption, |
||||
changeModeratorOption, |
||||
changeOwnerOption, |
||||
ignoreUserOption, |
||||
muteUserOption, |
||||
openDirectMessageOption, |
||||
removeUserOption, |
||||
videoCallOption, |
||||
]); |
||||
}; |
||||
@ -0,0 +1,4 @@ |
||||
import React from 'react'; |
||||
import { ModalBackdrop } from '@rocket.chat/fuselage'; |
||||
|
||||
export const Backdrop = (props) => <ModalBackdrop bg='transparent' {...props} />; |
||||
@ -0,0 +1,17 @@ |
||||
import React from 'react'; |
||||
|
||||
import { useTimezoneTime } from '../../hooks/useTimezoneTime'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
|
||||
const useUTCClock = (utcOffset) => { |
||||
const time = useTimezoneTime(utcOffset, 10000); |
||||
return `${ time } (UTC ${ utcOffset })`; |
||||
}; |
||||
|
||||
export const UTCClock = React.memo(({ utcOffset }) => useUTCClock(utcOffset)); |
||||
|
||||
export const LocalTime = React.memo(({ utcOffset }) => { |
||||
const t = useTranslation(); |
||||
const time = useUTCClock(utcOffset); |
||||
return t('Local_Time_time', { time }); |
||||
}); |
||||
@ -0,0 +1,98 @@ |
||||
import React, { forwardRef } from 'react'; |
||||
import { Box, Tag, Button, Icon, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
import { ActionButton } from './Buttons/ActionButton'; |
||||
import UserAvatar from './avatar/UserAvatar'; |
||||
import * as Status from './UserStatus'; |
||||
|
||||
const clampStyle = { |
||||
display: '-webkit-box', |
||||
overflow: 'hidden', |
||||
WebkitLineClamp: 3, |
||||
WebkitBoxOrient: 'vertical', |
||||
wordBreak: 'break-all', |
||||
}; |
||||
|
||||
export const Action = ({ icon, label, ...props }) => ( |
||||
<Button title={label} {...props} small mi='x2'> |
||||
<Icon name={icon} size='x16' /> |
||||
</Button> |
||||
); |
||||
|
||||
export const Info = (props) => ( |
||||
<Box |
||||
mb='x4' |
||||
is='span' |
||||
fontSize='p1' |
||||
fontScale='p1' |
||||
color='hint' |
||||
withTruncatedText |
||||
{...props} |
||||
/> |
||||
); |
||||
|
||||
export const Username = ({ name, status = <Status.Offline/> }) => <Box display='flex' flexShrink={0} alignItems='center' fontScale='s2' color='default' withTruncatedText> |
||||
{status} <Box mis='x8' flexGrow={1} withTruncatedText>{name}</Box> |
||||
</Box>; |
||||
|
||||
const Roles = ({ children }) => <Info rcx-user-card__roles m='neg-x2' flexWrap='wrap' display='flex' flexShrink={0}> |
||||
{children} |
||||
</Info>; |
||||
|
||||
const Role = ({ children }) => <Tag |
||||
pb={0} |
||||
m='x2' |
||||
disabled |
||||
fontScale='c2' |
||||
children={children} |
||||
/>; |
||||
|
||||
const UserCardConteiner = forwardRef((props, ref) => <Box rcx-user-card bg='surface' elevation='2' p='x24' display='flex' borderRadius='x2' width='439px' {...props} ref={ref}/>); |
||||
const UserCard = forwardRef(({ |
||||
className, |
||||
style, |
||||
open, |
||||
name = <Skeleton width='100%'/>, |
||||
username, |
||||
customStatus = <Skeleton width='100%'/>, |
||||
roles = <> |
||||
<Skeleton width='32%' mi='x2'/> |
||||
<Skeleton width='32%' mi='x2'/> |
||||
<Skeleton width='32%' mi='x2'/> |
||||
</>, |
||||
bio = <> |
||||
<Skeleton width='100%'/> |
||||
<Skeleton width='100%'/> |
||||
<Skeleton width='100%'/> |
||||
</>, |
||||
status = <Status.Offline/>, |
||||
actions, |
||||
localTime = <Skeleton width='100%'/>, |
||||
onClose, |
||||
t = (e) => e, |
||||
}, ref) => <UserCardConteiner className={className} ref={ref} style={style}> |
||||
<Box> |
||||
<UserAvatar username={username} size='x124'/> |
||||
{ actions && <Box flexGrow={0} display='flex' mb='x8' align='center' justifyContent='center'> |
||||
{actions} |
||||
</Box>} |
||||
</Box> |
||||
<Box display='flex' flexDirection='column' flexGrow={1} flexShrink={1} mis='x24' width='1px'> |
||||
<Username status={status} name={name}/> |
||||
{ customStatus && <Info>{customStatus}</Info> } |
||||
<Roles>{roles}</Roles> |
||||
<Info>{localTime}</Info> |
||||
{ bio && <Info withTruncatedText={false} style={clampStyle} height='x60'>{bio}</Info> } |
||||
{open && <a onClick={open}>{t('See_full_profile')}</a>} |
||||
</Box> |
||||
{onClose && <Box><ActionButton icon='cross' onClick={onClose}/></Box>} |
||||
</UserCardConteiner>); |
||||
|
||||
|
||||
export default UserCard; |
||||
|
||||
UserCard.Action = Action; |
||||
UserCard.Role = Role; |
||||
UserCard.Roles = Roles; |
||||
UserCard.Info = Info; |
||||
UserCard.Username = Username; |
||||
@ -0,0 +1,71 @@ |
||||
import React from 'react'; |
||||
|
||||
import UserCard from './UserCard'; |
||||
|
||||
export default { |
||||
title: 'components/UserCard', |
||||
component: UserCard, |
||||
}; |
||||
|
||||
const user = { |
||||
name: 'guilherme.gazzo', |
||||
customStatus: '🛴 currently working on User Card', |
||||
roles: [<UserCard.Role>Admin</UserCard.Role>, <UserCard.Role>Rocket.Chat</UserCard.Role>, <UserCard.Role>Team</UserCard.Role>], |
||||
bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus, eros convallis vulputate cursus, nisi neque eleifend libero, eget lacinia justo purus nec est. In at sodales ipsum. Sed lacinia quis purus eget pulvinar. Aenean eu pretium nunc, at aliquam magna. Praesent dignissim, tortor sed volutpat mattis, mauris diam pulvinar leo, porta commodo risus est non purus. Mauris in justo vel lorem ullamcorper hendrerit. Nam est metus, viverra a pellentesque vitae, ornare eget odio. Morbi tempor feugiat mattis. Morbi non felis tempor, aliquam justo sed, sagittis nibh. Mauris consequat ex metus. Praesent sodales sit amet nibh a vulputate. Integer commodo, mi vel bibendum sollicitudin, urna lectus accumsan ante, eget faucibus augue ex id neque. Aenean consectetur, orci a pellentesque mattis, tortor tellus fringilla elit, non ullamcorper risus nunc feugiat risus. Fusce sit amet nisi dapibus turpis commodo placerat. In tortor ante, vehicula sit amet augue et, imperdiet porta sem.', |
||||
actions: [<UserCard.Action icon='message'/>, <UserCard.Action icon='phone'/>], |
||||
localTime: 'Local Time: 7:44 AM', |
||||
}; |
||||
|
||||
const largeName = { |
||||
...user, |
||||
customStatus: |
||||
'🛴 currently working on User Card on User Card on User Card on User Card on User Card ', |
||||
name: |
||||
'guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.guilherme.gazzo.', |
||||
}; |
||||
|
||||
const noRoles = { |
||||
...user, |
||||
roles: null, |
||||
}; |
||||
|
||||
const noActions = { |
||||
...user, |
||||
actions: null, |
||||
}; |
||||
|
||||
const noLocalTime = { |
||||
...user, |
||||
localTime: null, |
||||
}; |
||||
|
||||
const noBio = { |
||||
...user, |
||||
bio: null, |
||||
}; |
||||
|
||||
const noBioNoLocalTime = { |
||||
...user, |
||||
bio: null, |
||||
localTime: null, |
||||
}; |
||||
|
||||
const noBioNoLocalTimeNoRoles = { |
||||
...user, |
||||
bio: null, |
||||
localTime: null, |
||||
roles: null, |
||||
}; |
||||
|
||||
|
||||
export const Basic = () => <UserCard {...user} />; |
||||
export const LargeName = () => <UserCard {...largeName} />; |
||||
|
||||
export const NoRoles = () => <UserCard {...noRoles} />; |
||||
export const NoActions = () => <UserCard {...noActions} />; |
||||
export const NoLocalTime = () => <UserCard {...noLocalTime} />; |
||||
export const NoBio = () => <UserCard {...noBio} />; |
||||
export const NoBioAndNoLocalTime = () => <UserCard {...noBioNoLocalTime} />; |
||||
export const NoBioNoLocalTimeNoRoles = () => <UserCard {...noBioNoLocalTimeNoRoles} />; |
||||
|
||||
export const Loading = () => <UserCard />; |
||||
@ -0,0 +1,114 @@ |
||||
import React from 'react'; |
||||
import { Box, Margins, Tag, Button, Icon } from '@rocket.chat/fuselage'; |
||||
import { css } from '@rocket.chat/css-in-js'; |
||||
|
||||
import { useTimeAgo } from '../../hooks/useTimeAgo'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import VerticalBar from './VerticalBar'; |
||||
import { UTCClock } from './UTCClock'; |
||||
import UserAvatar from './avatar/UserAvatar'; |
||||
import UserCard from './UserCard'; |
||||
import MarkdownText from './MarkdownText'; |
||||
|
||||
const Label = (props) => <Box fontScale='p2' color='default' {...props} />; |
||||
|
||||
const wordBreak = css` |
||||
word-break: break-word; |
||||
`;
|
||||
|
||||
const Info = ({ className, ...props }) => <UserCard.Info className={[className, wordBreak]} flexShrink={0} {...props}/>; |
||||
|
||||
export const UserInfo = React.memo(function UserInfo({ |
||||
username, |
||||
bio, |
||||
email, |
||||
status, |
||||
phone, |
||||
customStatus, |
||||
roles = [], |
||||
lastLogin, |
||||
createdAt, |
||||
utcOffset, |
||||
customFields = [], |
||||
name, |
||||
data, |
||||
// onChange,
|
||||
actions, |
||||
...props |
||||
}) { |
||||
const t = useTranslation(); |
||||
|
||||
const timeAgo = useTimeAgo(); |
||||
|
||||
return <VerticalBar.ScrollableContent p='x24' {...props}> |
||||
|
||||
<UserAvatar size={'x332'} title={username} username={username}/> |
||||
|
||||
{actions} |
||||
|
||||
<Margins block='x4'> |
||||
<UserCard.Username name={username} status={status} /> |
||||
<Info>{customStatus}</Info> |
||||
|
||||
{!!roles && <> |
||||
<Label>{t('Roles')}</Label> |
||||
<UserCard.Roles>{roles}</UserCard.Roles> |
||||
</>} |
||||
|
||||
{Number.isInteger(utcOffset) && <> |
||||
<Label>{t('Local Time')}</Label> |
||||
<Info><UTCClock utcOffset={utcOffset}/></Info> |
||||
</>} |
||||
|
||||
<Label>{t('Last_login')}</Label> |
||||
<Info>{lastLogin ? timeAgo(lastLogin) : t('Never')}</Info> |
||||
|
||||
{name && <> |
||||
<Label>{t('Full Name')}</Label> |
||||
<Info>{name}</Info> |
||||
</>} |
||||
|
||||
{bio && <> |
||||
<Label>{t('Bio')}</Label> |
||||
<Info withTruncatedText={false}><MarkdownText content={bio}/></Info> |
||||
</>} |
||||
|
||||
{phone && <> <Label>{t('Phone')}</Label> |
||||
<Info display='flex' flexDirection='row' alignItems='center'> |
||||
<Box is='a' withTruncatedText href={`tel:${ phone }`}>{phone}</Box> |
||||
</Info> |
||||
</>} |
||||
|
||||
{email && <> <Label>{t('Email')}</Label> |
||||
<Info display='flex' flexDirection='row' alignItems='center'> |
||||
<Box is='a' withTruncatedText href={`mailto:${ email.address }`}>{email.address}</Box> |
||||
<Margins inline='x4'> |
||||
{email.verified && <Tag variant='primary'>{t('Verified')}</Tag>} |
||||
{email.verified || <Tag disabled>{t('Not_verified')}</Tag>} |
||||
</Margins> |
||||
</Info> |
||||
</>} |
||||
|
||||
{ customFields && customFields.map((customField) => <React.Fragment key={customField.label}> |
||||
<Label>{t(customField.label)}</Label> |
||||
<Info>{customField.value}</Info> |
||||
</React.Fragment>) } |
||||
|
||||
<Label>{t('Created_at')}</Label> |
||||
<Info>{timeAgo(createdAt)}</Info> |
||||
|
||||
</Margins> |
||||
|
||||
</VerticalBar.ScrollableContent>; |
||||
}); |
||||
|
||||
export const Action = ({ icon, label, ...props }) => ( |
||||
<Button title={label} {...props} mi='x4'> |
||||
<Icon name={icon} size='x20' mie='x4' /> |
||||
{label} |
||||
</Button> |
||||
); |
||||
|
||||
UserInfo.Action = Action; |
||||
|
||||
export default UserInfo; |
||||
@ -0,0 +1,26 @@ |
||||
import React from 'react'; |
||||
|
||||
import { UserInfo } from './UserInfo'; |
||||
import VerticalBar from './VerticalBar'; |
||||
|
||||
export default { |
||||
title: 'components/UserInfo', |
||||
component: UserInfo, |
||||
}; |
||||
|
||||
const user = { |
||||
name: 'Guilherme Gazzo', |
||||
username: 'guilherme.gazzo', |
||||
customStatus: '🛴 currently working on User Card', |
||||
bio: |
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempus, eros convallis vulputate cursus, nisi neque eleifend libero, eget lacinia justo purus nec est. In at sodales ipsum. Sed lacinia quis purus eget pulvinar. Aenean eu pretium nunc, at aliquam magna. Praesent dignissim, tortor sed volutpat mattis, mauris diam pulvinar leo, porta commodo risus est non purus. Mauris in justo vel lorem ullamcorper hendrerit. Nam est metus, viverra a pellentesque vitae, ornare eget odio. Morbi tempor feugiat mattis. Morbi non felis tempor, aliquam justo sed, sagittis nibh. Mauris consequat ex metus. Praesent sodales sit amet nibh a vulputate. Integer commodo, mi vel bibendum sollicitudin, urna lectus accumsan ante, eget faucibus augue ex id neque. Aenean consectetur, orci a pellentesque mattis, tortor tellus fringilla elit, non ullamcorper risus nunc feugiat risus. Fusce sit amet nisi dapibus turpis commodo placerat. In tortor ante, vehicula sit amet augue et, imperdiet porta sem.', |
||||
// actions: [<UserCard.Action icon='message'/>, <UserCard.Action icon='phone'/>],
|
||||
localTime: 'Local Time: 7:44 AM', |
||||
utcOffset: -3, |
||||
email: { |
||||
address: 'rocketchat@rocket.chat', |
||||
verified: true, |
||||
}, |
||||
}; |
||||
|
||||
export const Default = () => <VerticalBar><UserInfo { ...user } /></VerticalBar>; |
||||
@ -0,0 +1,23 @@ |
||||
import React from 'react'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
|
||||
const Base = (props) => <Box size='x12' borderRadius='full' flexShrink={0} {...props}/>; |
||||
|
||||
export const Busy = () => <Base bg='danger-500'/>; |
||||
export const Away = () => <Base bg='warning-600'/>; |
||||
export const Online = () => <Base bg='success-500'/>; |
||||
export const Offline = () => <Base bg='neutral-600'/>; |
||||
|
||||
|
||||
export const getStatus = (status) => { |
||||
switch (status) { |
||||
case 'online': |
||||
return <Online/>; |
||||
case 'busy': |
||||
return <Busy/>; |
||||
case 'away': |
||||
return <Away/>; |
||||
default: |
||||
return <Offline/>; |
||||
} |
||||
}; |
||||
@ -0,0 +1,14 @@ |
||||
import React from 'react'; |
||||
|
||||
import * as Status from './UserStatus'; |
||||
|
||||
export default { |
||||
title: 'components/UserStatus', |
||||
component: Status, |
||||
}; |
||||
|
||||
|
||||
export const Online = () => <Status.Online />; |
||||
export const Away = () => <Status.Away />; |
||||
export const Busy = () => <Status.Busy />; |
||||
export const Offline = () => <Status.Offline />; |
||||
@ -1,8 +1,9 @@ |
||||
import React from 'react'; |
||||
import { Avatar } from '@rocket.chat/fuselage'; |
||||
|
||||
import BaseAvatar from './BaseAvatar'; |
||||
|
||||
const objectFit = { objectFit: 'contain' }; |
||||
|
||||
export default function AppAvatar({ iconFileContent, iconFileData, ...props }) { |
||||
return <Avatar style={objectFit} url={iconFileContent || `data:image/png;base64,${ iconFileData }`} {...props}/>; |
||||
return <BaseAvatar style={objectFit} url={iconFileContent || `data:image/png;base64,${ iconFileData }`} {...props}/>; |
||||
} |
||||
|
||||
@ -0,0 +1,14 @@ |
||||
import React, { useState } from 'react'; |
||||
import { Avatar, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
function BaseAvatar(props) { |
||||
const [error, setError] = useState(false); |
||||
|
||||
if (error) { |
||||
return <Skeleton variant='rect' {...props} />; |
||||
} |
||||
|
||||
return <Avatar onError={setError} {...props}/>; |
||||
} |
||||
|
||||
export default BaseAvatar; |
||||
@ -1,12 +1,12 @@ |
||||
import React from 'react'; |
||||
import { Avatar } from '@rocket.chat/fuselage'; |
||||
|
||||
import { roomTypes } from '../../../../app/utils/client'; |
||||
import BaseAvatar from './BaseAvatar'; |
||||
|
||||
function RoomAvatar({ room: { type, ...room }, ...props }) { |
||||
const avatarUrl = roomTypes.getConfig(type).getAvatarPath({ type, ...room }); |
||||
|
||||
return <Avatar url={avatarUrl} title={avatarUrl} {...props}/>; |
||||
return <BaseAvatar url={avatarUrl} title={avatarUrl} {...props}/>; |
||||
} |
||||
|
||||
export default RoomAvatar; |
||||
|
||||
@ -0,0 +1,14 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
type SubscriptionContextValue = { |
||||
useUserSubscription: (rid: string, fields: Mongo.Query<any>) => any; |
||||
useUserSubscriptionByName: (name: string, fields: Mongo.Query<any>) => any; |
||||
}; |
||||
|
||||
export const SubscriptionContext = createContext<SubscriptionContextValue>({ |
||||
useUserSubscription: () => ({}), |
||||
useUserSubscriptionByName: () => ({}), |
||||
}); |
||||
|
||||
export const useUserSubscription = (rid: string, fields: Mongo.Query<any>): Mongo.Collection<any> => useContext(SubscriptionContext).useUserSubscription(rid, fields); |
||||
export const useUserSubscriptionByName = (name: string, fields: Mongo.Query<any>): Mongo.Collection<any> => useContext(SubscriptionContext).useUserSubscriptionByName(name, fields); |
||||
@ -0,0 +1,20 @@ |
||||
import { useState, useEffect } from 'react'; |
||||
import moment from 'moment'; |
||||
|
||||
import { useFormatTime } from './useFormatTime'; |
||||
|
||||
export const useTimezoneTime = (offset, interval = 1000) => { |
||||
const [time, setTime] = useState(null); |
||||
const format = useFormatTime(); |
||||
useEffect(() => { |
||||
if (offset === undefined) { |
||||
return; |
||||
} |
||||
const update = () => setTime(moment().utcOffset(offset)); |
||||
const timer = setInterval(update, interval); |
||||
update(); |
||||
return () => clearInterval(timer); |
||||
}, [offset, interval]); |
||||
|
||||
return format(time); |
||||
}; |
||||
@ -0,0 +1,20 @@ |
||||
import React, { useMemo, FC, useCallback } from 'react'; |
||||
|
||||
import { SubscriptionContext } from '../contexts/SubscriptionContext'; |
||||
import { useReactiveValue } from '../hooks/useReactiveValue'; |
||||
import { Subscriptions } from '../../app/models/client'; |
||||
|
||||
export const useUserSubscription = (rid: string, fields: Mongo.Query<any>): Mongo.Collection<any> => useReactiveValue(useCallback(() => Subscriptions.findOne({ rid }, { fields }), [rid, fields])); |
||||
export const useUserSubscriptionByName = (name: string, fields: Mongo.Query<any>): Mongo.Collection<any> => useReactiveValue(useCallback(() => Subscriptions.findOne({ name }, { fields }), [name, fields])); |
||||
|
||||
|
||||
const SubscriptionProvider: FC = ({ children }) => { |
||||
const contextValue = useMemo(() => ({ |
||||
useUserSubscription, |
||||
useUserSubscriptionByName, |
||||
}), []); |
||||
|
||||
return <SubscriptionContext.Provider children={children} value={contextValue} />; |
||||
}; |
||||
|
||||
export default SubscriptionProvider; |
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue