diff --git a/app/theme/client/imports/components/contextual-bar.css b/app/theme/client/imports/components/contextual-bar.css index f5bb2b4b0ad..ecc6e8a8be1 100644 --- a/app/theme/client/imports/components/contextual-bar.css +++ b/app/theme/client/imports/components/contextual-bar.css @@ -1,18 +1,33 @@ .contextual-bar { - z-index: 10; + &.contextual-bar { + z-index: 10; - display: flex; + display: flex; + + overflow: hidden; + flex-direction: column; + flex: 0 0 var(--flex-tab-width); - overflow: hidden; - flex-direction: column; - flex: 0 0 var(--flex-tab-width); + width: var(--flex-tab-width); + height: 100%; - width: var(--flex-tab-width); - height: 100%; + background: var(--color-white); - background: var(--color-white); + border-inline-start: 2px solid var(--color-gray-lightest); - border-inline-start: 2px solid var(--color-gray-lightest); + & > .flex-tab { + width: 100%; + } + + & & { + margin-left: -2px; + } + + .rtl & & { + margin-right: -2px; + margin-left: 0; + } + } &-wrap { position: relative; @@ -24,10 +39,6 @@ max-height: 100%; } - & > .flex-tab { - width: 100%; - } - &__content { display: flex; overflow: auto; diff --git a/app/ui-flextab/client/index.js b/app/ui-flextab/client/index.js index 1e5bd021637..3def11b4ad8 100644 --- a/app/ui-flextab/client/index.js +++ b/app/ui-flextab/client/index.js @@ -3,12 +3,9 @@ import './tabs/inviteUsers.html'; import './tabs/createInviteLink.html'; import './tabs/membersList.html'; import './tabs/uploadedFilesList.html'; -import './tabs/userEdit.html'; import './flexTabBar'; import './tabs/inviteUsers'; import './tabs/createInviteLink'; import './tabs/membersList'; import './tabs/uploadedFilesList'; -import './tabs/userEdit'; -import './tabs/userInfo'; import './tabs/keyboardShortcuts.html'; diff --git a/app/ui-flextab/client/tabs/membersList.html b/app/ui-flextab/client/tabs/membersList.html index 27fa9b62432..e7708dd1426 100644 --- a/app/ui-flextab/client/tabs/membersList.html +++ b/app/ui-flextab/client/tabs/membersList.html @@ -60,7 +60,9 @@ {{/with}} {{/if}} + {{#if userInfoDetail}}
- {{> userInfo (userInfoDetail)}} + {{> UserInfoWithData (userInfoDetail)}}
+ {{/if}} diff --git a/app/ui-flextab/client/tabs/membersList.js b/app/ui-flextab/client/tabs/membersList.js index 54d8bf1a297..f7e8c22d4a8 100644 --- a/app/ui-flextab/client/tabs/membersList.js +++ b/app/ui-flextab/client/tabs/membersList.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; +import { HTML } from 'meteor/htmljs'; import { getActions } from './userActions'; import { RoomManager, popover } from '../../../ui-utils/client'; @@ -10,6 +11,12 @@ import { settings } from '../../../settings/client'; import { t, isRtl, handleError, roomTypes, getUserAvatarURL } from '../../../utils/client'; import { WebRTC } from '../../../webrtc/client'; import { hasPermission } from '../../../authorization/client'; +import { createTemplateForComponent } from '../../../../client/reactAdapters'; + +createTemplateForComponent('UserInfoWithData', () => import('../../../../client/channel/UserInfo'), { + // eslint-disable-next-line new-cap + renderContainerView: () => HTML.DIV({ class: 'contextual-bar', style: 'flex-grow: 1;' }), +}); Template.membersList.helpers({ ignored() { @@ -107,13 +114,15 @@ Template.membersList.helpers({ userInfoDetail() { const room = ChatRoom.findOne(this.rid, { fields: { t: 1, usernames: 1 } }); - + const username = Template.instance().userDetail.get(); + if (!username) { + return; + } return { tabBar: Template.currentData().tabBar, - username: Template.instance().userDetail.get(), - clear: Template.instance().clearUserDetail, - showAll: roomTypes.getConfig(room.t).userDetailShowAll(room) || false, - hideAdminControls: roomTypes.getConfig(room.t).userDetailShowAdmin(room) || false, + username, + rid: Template.currentData().rid, + onClose: Template.instance().clearUserDetail, video: ['d'].includes(room && room.t), showBackButton: roomTypes.getConfig(room.t).isGroupChat(room), }; @@ -291,6 +300,7 @@ Template.membersList.onCreated(function() { this.clearUserDetail = () => { this.showDetail.set(false); + this.userDetail.set(null); this.tabBar.setData({ label: 'Members', icon: 'team', @@ -316,7 +326,7 @@ Template.membersList.onCreated(function() { }); Template.membersList.onRendered(function() { - this.firstNode.parentNode.querySelector('#user-search').focus(); + this.firstNode.parentNode.querySelector('#user-search')?.focus(); this.autorun(() => { const showAllUsers = this.showAllUsers.get(); const statusTypeSelect = this.find('.js-type'); diff --git a/app/ui-flextab/client/tabs/userEdit.html b/app/ui-flextab/client/tabs/userEdit.html deleted file mode 100644 index f4e17c2aef7..00000000000 --- a/app/ui-flextab/client/tabs/userEdit.html +++ /dev/null @@ -1,215 +0,0 @@ - diff --git a/app/ui-flextab/client/tabs/userEdit.js b/app/ui-flextab/client/tabs/userEdit.js deleted file mode 100644 index ec37c0d816a..00000000000 --- a/app/ui-flextab/client/tabs/userEdit.js +++ /dev/null @@ -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); - }); - }; -}); diff --git a/app/ui-flextab/client/tabs/userInfo.html b/app/ui-flextab/client/tabs/userInfo.html deleted file mode 100644 index 6071ec168a8..00000000000 --- a/app/ui-flextab/client/tabs/userInfo.html +++ /dev/null @@ -1,128 +0,0 @@ - diff --git a/app/ui-flextab/client/tabs/userInfo.js b/app/ui-flextab/client/tabs/userInfo.js deleted file mode 100644 index 0ca0c4add07..00000000000 --- a/app/ui-flextab/client/tabs/userInfo.js +++ /dev/null @@ -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); -}); diff --git a/app/ui/client/lib/UserCard.js b/app/ui/client/lib/UserCard.js new file mode 100644 index 00000000000..75123493e0d --- /dev/null +++ b/app/ui/client/lib/UserCard.js @@ -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); +}; diff --git a/app/ui/client/views/app/room.js b/app/ui/client/views/app/room.js index a84c2be709a..aa24131cdfd 100644 --- a/app/ui/client/views/app/room.js +++ b/app/ui/client/views/app/room.js @@ -37,6 +37,7 @@ import { ChatMessages } from '../../lib/chatMessages'; import { fileUpload } from '../../lib/fileUpload'; import { isURL } from '../../../../utils/lib/isURL'; import { mime } from '../../../../utils/lib/mimeTypes'; +import { openUserCard } from '../../lib/UserCard'; export const chatMessages = {}; @@ -72,23 +73,23 @@ const openProfileTab = (e, instance, username) => { }; const openProfileTabOrOpenDM = (e, instance, username) => { - if (settings.get('UI_Click_Direct_Message')) { - Meteor.call('createDirectMessage', username, (error, result) => { - if (error) { - if (error.isClientSafe) { - openProfileTab(e, instance, username); - } else { - handleError(error); - } - } - - if (result && result.rid) { - FlowRouter.go('direct', { rid: result.rid }, FlowRouter.current().queryParams); - } - }); - } else { - openProfileTab(e, instance, username); - } + // if (settings.get('UI_Click_Direct_Message')) { + // Meteor.call('createDirectMessage', username, (error, result) => { + // if (error) { + // if (error.isClientSafe) { + // openProfileTab(e, instance, username); + // } else { + // handleError(error); + // } + // } + + // if (result && result.rid) { + // FlowRouter.go('direct', { rid: result.rid }, FlowRouter.current().queryParams); + // } + // }); + // } else { + openProfileTab(e, instance, username); + // } e.stopPropagation(); }; @@ -829,17 +830,6 @@ Template.room.events({ openProfileTabOrOpenDM(e, instance, this.user.username); }, - 'click .user-card-message'(e, instance) { - const { msg } = messageArgs(this); - if (!Meteor.userId()) { - return; - } - - const { username } = msg.u; - - openProfileTabOrOpenDM(e, instance, username); - }, - 'scroll .wrapper': _.throttle(function(e, t) { const $roomLeader = $('.room-leader'); if ($roomLeader.length) { @@ -936,10 +926,41 @@ Template.room.events({ } if (username) { - openProfileTabOrOpenDM(e, instance, username); + openUserCard({ + username, + rid: Session.get('openedRoom'), + target: e.currentTarget, + open: (e) => { + e.preventDefault(); + openProfileTabOrOpenDM(e, instance, username); + }, + }); + } + }, + + + 'click .user-card-message'(e, instance) { + const { msg } = messageArgs(this); + if (!Meteor.userId()) { + return; + } + + const { username } = msg.u; + + if (username) { + openUserCard({ + username, + rid: Session.get('openedRoom'), + target: e.currentTarget, + open: (e) => { + e.preventDefault(); + openProfileTabOrOpenDM(e, instance, username); + }, + }); } }, + 'click .image-to-download'(event) { const { msg } = messageArgs(this); ChatMessage.update({ _id: msg._id, 'urls.url': $(event.currentTarget).data('url') }, { $set: { 'urls.$.downloadImages': true } }); @@ -1071,8 +1092,8 @@ Template.room.onCreated(function() { if (roomTypes.getConfig(room.t).isGroupChat(room)) { return; } - - this.userDetail.set(room.usernames.filter((username) => username !== user.username)[0]); + const usernames = Array.from(new Set(room.usernames)); + this.userDetail.set(this.userDetail.get() || (usernames.length === 1 ? usernames[0] : usernames.filter((username) => username !== user.username)[0])); }); this.autorun(() => { @@ -1155,12 +1176,9 @@ Template.room.onCreated(function() { return previewMessages; }; - this.setUserDetail = (username) => { - this.userDetail.set(username); - }; - this.clearUserDetail = () => { this.userDetail.set(null); + this.tabBar.close(); }; Meteor.call('getRoomRoles', this.data._id, function(error, results) { diff --git a/client/Channel/hooks/useUserSubscription.js b/client/Channel/hooks/useUserSubscription.js deleted file mode 100644 index 3d9d96e0356..00000000000 --- a/client/Channel/hooks/useUserSubscription.js +++ /dev/null @@ -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])); diff --git a/client/admin/customEmoji/CustomEmojiRoute.js b/client/admin/customEmoji/CustomEmojiRoute.js index 5a31c75a987..646b22a0856 100644 --- a/client/admin/customEmoji/CustomEmojiRoute.js +++ b/client/admin/customEmoji/CustomEmojiRoute.js @@ -1,6 +1,6 @@ import React, { useMemo, useState, useCallback } from 'react'; import { Button, Icon } from '@rocket.chat/fuselage'; -import { useDebouncedValue, useMediaQuery } from '@rocket.chat/fuselage-hooks'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { usePermission } from '../../contexts/AuthorizationContext'; import { useTranslation } from '../../contexts/TranslationContext'; @@ -42,9 +42,6 @@ export default function CustomEmojiRoute({ props }) { const router = useRoute(routeName); - const mobile = useMediaQuery('(max-width: 420px)'); - const small = useMediaQuery('(max-width: 780px)'); - const context = useRouteParameter('context'); const id = useRouteParameter('id'); @@ -93,15 +90,14 @@ export default function CustomEmojiRoute({ props }) { { context - && + && { context === 'edit' && t('Custom_Emoji_Info') } { context === 'new' && t('Custom_Emoji_Add') } - - - {context === 'edit' && } - {context === 'new' && } - + + + {context === 'edit' && } + {context === 'new' && } } ; } diff --git a/client/admin/customSounds/AdminSoundsRoute.js b/client/admin/customSounds/AdminSoundsRoute.js index 3056e8efa60..af3f832fe14 100644 --- a/client/admin/customSounds/AdminSoundsRoute.js +++ b/client/admin/customSounds/AdminSoundsRoute.js @@ -2,7 +2,7 @@ import React, { useMemo, useState, useCallback } from 'react'; import { Button, Icon } from '@rocket.chat/fuselage'; -import { useDebouncedValue, useMediaQuery } from '@rocket.chat/fuselage-hooks'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { usePermission } from '../../contexts/AuthorizationContext'; import { useTranslation } from '../../contexts/TranslationContext'; @@ -45,9 +45,6 @@ export default function CustomSoundsRoute({ props }) { const router = useRoute(routeName); - const mobile = useMediaQuery('(max-width: 420px)'); - const small = useMediaQuery('(max-width: 780px)'); - const context = useRouteParameter('context'); const id = useRouteParameter('id'); @@ -96,15 +93,14 @@ export default function CustomSoundsRoute({ props }) { { context - && + && { context === 'edit' && t('Custom_Sound_Edit') } { context === 'new' && t('Custom_Sound_Add') } - - - {context === 'edit' && } - {context === 'new' && } - + + + {context === 'edit' && } + {context === 'new' && } } ; } diff --git a/client/admin/customUserStatus/CustomUserStatusRoute.js b/client/admin/customUserStatus/CustomUserStatusRoute.js index aca0a1fdac2..d4a1a24bad7 100644 --- a/client/admin/customUserStatus/CustomUserStatusRoute.js +++ b/client/admin/customUserStatus/CustomUserStatusRoute.js @@ -1,6 +1,6 @@ import React, { useMemo, useState, useCallback } from 'react'; import { Button, Icon } from '@rocket.chat/fuselage'; -import { useDebouncedValue, useMediaQuery } from '@rocket.chat/fuselage-hooks'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { usePermission } from '../../contexts/AuthorizationContext'; import { useTranslation } from '../../contexts/TranslationContext'; @@ -42,9 +42,6 @@ export default function CustomUserStatusRoute({ props }) { const router = useRoute(routeName); - const mobile = useMediaQuery('(max-width: 420px)'); - const small = useMediaQuery('(max-width: 780px)'); - const context = useRouteParameter('context'); const id = useRouteParameter('id'); @@ -93,15 +90,15 @@ export default function CustomUserStatusRoute({ props }) { { context - && + && { context === 'edit' && t('Custom_User_Status_Edit') } { context === 'new' && t('Custom_User_Status_Add') } - - - {context === 'edit' && } - {context === 'new' && } - + + + + {context === 'edit' && } + {context === 'new' && } } ; } diff --git a/client/admin/rooms/RoomsPage.js b/client/admin/rooms/RoomsPage.js index 515bdea3101..09eb4b140dc 100644 --- a/client/admin/rooms/RoomsPage.js +++ b/client/admin/rooms/RoomsPage.js @@ -26,14 +26,13 @@ export function RoomsPage() { - {context && + {context && {t('Room_Info')} - - - + + } ; } diff --git a/client/admin/sidebar/AdminSidebar.js b/client/admin/sidebar/AdminSidebar.js index 3ecd950375d..15fb1c1adc6 100644 --- a/client/admin/sidebar/AdminSidebar.js +++ b/client/admin/sidebar/AdminSidebar.js @@ -83,7 +83,6 @@ const AdminSidebarSettings = ({ currentPath }) => { placeholder={t('Search')} onChange={handleChange} addon={} - className={['asdsads']} /> diff --git a/client/admin/users/UserInfo.js b/client/admin/users/UserInfo.js index 50b74186530..7e2346a7536 100644 --- a/client/admin/users/UserInfo.js +++ b/client/admin/users/UserInfo.js @@ -1,42 +1,55 @@ -import React, { useMemo, useState, useEffect, useCallback } from 'react'; -import { Box, Margins, Chip, Tag } from '@rocket.chat/fuselage'; -import moment from 'moment'; +import React, { useMemo, useState } 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 { DateFormat } from '../../../app/lib'; +import { useSetting } from '../../contexts/SettingsContext'; +import * as UserStatus from '../../components/basic/UserStatus'; +import UserCard from '../../components/basic/UserCard'; import { UserInfoActions } from './UserInfoActions'; -import MarkdownText from '../../components/basic/MarkdownText'; -import VerticalBar from '../../components/basic/VerticalBar'; import { FormSkeleton } from './Skeleton'; -import UserAvatar from '../../components/basic/avatar/UserAvatar'; -const useTimezoneClock = (utcOffset = 0, updateInterval) => { - const [time, setTime] = useState(); - useEffect(() => { - const updateTime = () => setTime(DateFormat.formatTime(moment().get().utcOffset(utcOffset))); - const interval = setInterval(() => updateTime(), updateInterval); - updateTime(); - - return () => clearInterval(interval); - }, [utcOffset, updateInterval]); - - return time; -}; - -const UTCClock = ({ utcOffset, ...props }) => { - const time = useTimezoneClock(utcOffset, 10000); - return {time} UTC {utcOffset}; -}; - -export function UserInfoWithData({ uid, ...props }) { +export function UserInfoWithData({ uid, username, ...props }) { const t = useTranslation(); const [cache, setCache] = useState(); + const showRealNames = useSetting('UI_Use_Real_Name'); + const approveManuallyUsers = useSetting('Accounts_ManuallyApproveNewUsers'); const onChange = () => setCache(new Date()); // TODO: remove cache. Is necessary for data invalidation - const { data, state, error } = useEndpointDataExperimental('users.info', useMemo(() => ({ userId: uid }), [uid, cache])); + const { data, state, error } = useEndpointDataExperimental('users.info', useMemo(() => ({ ...uid && { userId: uid }, ...username && { username } }), [uid, username, cache])); + + 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) => ( + {role} + )), + bio, + phone: user.phone, + utcOffset, + customFields: [approveManuallyUsers && user.active === false && user.reason && { label: 'Reason', value: user.reason }, ...Array.isArray(user.customFields) ? user.customFields : []].filter(Boolean), + email: user.emails?.find(({ address }) => !!address)?.address, + createdAt: user.createdAt, + status: UserStatus.getStatus(status), + customStatus: statusText, + }; + }, [data, showRealNames]); if (state === ENDPOINT_STATES.LOADING) { return ; @@ -46,62 +59,11 @@ export function UserInfoWithData({ uid, ...props }) { return {t('User_not_found')}; } - return ; -} - - -export function UserInfo({ data, onChange, ...props }) { - const t = useTranslation(); - - const createdAt = DateFormat.formatDateAndTime(data.createdAt); - - const lastLogin = data.lastLogin ? DateFormat.formatDateAndTime(data.lastLogin) : ''; - - - return e.preventDefault(), [])} {...props}> - - - - {data.name || data.username}{data.nickname && ` (${ data.nickname })`} - {!!data.name && @{data.username}} - {data.status} - - - - - - - {data.bio && data.bio.trim().length > 0 && } - {!!data.roles.length && <> - {t('Roles')} - - - {data.roles.map((val) => {val})} - - - } - - {data.emails && <> {t('Email')} - - {data.emails[0].address} - - {data.emails[0].verified && {t('Verified')}} - {data.emails[0].verified || {t('Not_verified')}} - - - } - - {t('Created_at')} - {createdAt} - - {t('Last_login')} - {lastLogin || t('Never')} - - {!!data.utcOffset && <> - {t('Timezone')} - - } - - - ; + return } + {...props} + />; } diff --git a/client/admin/users/UserInfoActions.js b/client/admin/users/UserInfoActions.js index ed432884b19..92d9239d42e 100644 --- a/client/admin/users/UserInfoActions.js +++ b/client/admin/users/UserInfoActions.js @@ -1,14 +1,16 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { Box, Button, ButtonGroup, Icon, Menu } from '@rocket.chat/fuselage'; +import { Button, ButtonGroup, Icon, Menu, Modal, Option } from '@rocket.chat/fuselage'; +import React, { useCallback, useMemo } from 'react'; -import { Modal } from '../../components/basic/Modal'; -import { useTranslation } from '../../contexts/TranslationContext'; -import { useRoute } from '../../contexts/RouterContext'; +import { useUserInfoActionsSpread } from '../../channel/hooks/useUserInfoActions'; +import ConfirmOwnerChangeWarningModal from '../../components/ConfirmOwnerChangeWarningModal'; +import UserInfo from '../../components/basic/UserInfo'; import { usePermission } from '../../contexts/AuthorizationContext'; -import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; +import { useSetModal } from '../../contexts/ModalContext'; +import { useRoute } from '../../contexts/RouterContext'; import { useMethod, useEndpoint } from '../../contexts/ServerContext'; import { useSetting } from '../../contexts/SettingsContext'; -import ConfirmOwnerChangeWarningModal from '../../components/ConfirmOwnerChangeWarningModal'; +import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; +import { useTranslation } from '../../contexts/TranslationContext'; const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => { const t = useTranslation(); @@ -51,10 +53,9 @@ const SuccessModal = ({ onClose, ...props }) => { ; }; - -export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, ...props }) => { +export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange }) => { const t = useTranslation(); - const [modal, setModal] = useState(); + const setModal = useSetModal(); const directRoute = useRoute('direct'); const userRoute = useRoute('admin-users'); @@ -111,7 +112,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, .. const confirmDeleteUser = useCallback(() => { setModal( setModal()}/>); - }, [deleteUser]); + }, [deleteUser, setModal]); const setAdminStatus = useMethod('setAdminStatus'); const changeAdminStatus = useCallback(() => { @@ -159,25 +160,30 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, .. id: _id, }), [_id, userRoute]); - const menuOptions = useMemo(() => ({ + const options = useMemo(() => ({ ...canDirectMessage && { directMessage: { - label: <>{t('Direct_Message')}, + icon: 'chat', + label: t('Direct_Message'), action: directMessageClick, } }, ...canEditOtherUserInfo && { editUser: { - label: <>{t('Edit')}, + icon: 'edit', + label: t('Edit'), action: editUserClick, } }, ...canAssignAdminRole && { makeAdmin: { - label: <>{ isAdmin ? t('Remove_Admin') : t('Make_Admin')}, + icon: 'key', + label: isAdmin ? t('Remove_Admin') : t('Make_Admin'), action: changeAdminStatus, } }, ...canDeleteUser && { delete: { - label: {t('Delete')}, + icon: 'trash', + label: t('Delete'), action: confirmDeleteUser, } }, ...canEditOtherUserActiveStatus && { changeActiveStatus: { - label: <>{ isActive ? t('Deactivate') : t('Activate')}, + icon: 'user', + label: isActive ? t('Deactivate') : t('Activate'), action: changeActiveStatus, } }, }), [ @@ -196,22 +202,13 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, .. changeActiveStatus, ]); - const [actions, moreActions] = useMemo(() => { - const keys = Object.keys(menuOptions); + const { actions: actionsDefinition, menu: menuOptions } = useUserInfoActionsSpread(options); - const firstHalf = keys.slice(0, 2); - const secondHalf = keys.slice(2, keys.length); + const menu = menuOptions &&