[IMPROVE] New sidebar layout (#19089)
* wip * wip * more work in progress * lint * Fix IE11 support livechat widget * More wip * lint * Add correct buttons and fix some errors * fix import * Fix error with empty department agents * fix title and tags * more fixes * fix agents save * Fix agentlist not saving * First Review * update fuselage * Sidebar variations * Fix Stories * Sidebar Header * Initial data * Fix paddings * sidebar search * Wip Chats * Added more logic * Fix Memo * Virtual List * switch to VariableSizeList * Fix Size * Te acalma Gabriel * Badges * Menu actions * Do not group by type option * Highligthed state * Fix menu * Item Skeletons * Search list * Omnichannel to virtualList * Sidebar header * SidebarHeader * Better Ominichannel Context usage * Revome livechat template * Remove discussion Room List * alert and open prop * Menu as renderprop * ReactiveUserPresence * Update components * Update cachedCollection * Fiz discussions * update cachedcolletion * Header color * Fix unread * Presence * Fix presence * Fix Admin * [wip] Search bar * get usernames in subscription * Local an spotlight search * Fix avatar id prop * Fix multi users on search * Livechat RoomMenu * Fix Header in anonymous sessions * Fix sidebar * update base old * Sidebar variations * Fix Stories * Sidebar Header * Initial data * Fix paddings * sidebar search * Wip Chats * Added more logic * Virtual List * Fix Memo * switch to VariableSizeList * Fix Size * Te acalma Gabriel * Badges * Menu actions * Do not group by type option * Highligthed state * Item Skeletons * Search list * Fix menu * Omnichannel to virtualList * Sidebar header * SidebarHeader * Better Ominichannel Context usage * Revome livechat template * Remove discussion Room List * alert and open prop * Menu as renderprop * ReactiveUserPresence * Update components * Update cachedCollection * Fiz discussions * update cachedcolletion * Header color * Fix unread * Presence * Fix presence * Fix Admin * [wip] Search bar * get usernames in subscription * Local an spotlight search * Fix avatar id prop * [FIX] Missing "Bio" in user's profile view (#19166) * [FIX] Omnichannel: triggers page not rendering (#19134) * [FIX] VisitorAutoComplete component (#19133) * [FIX] Admin Sidebar overflowing (#19101) * [FIX] Integrations history page not reacting to changes. (#19114) * [FIX] Selecting the same department for multiple units (#19168) * [FIX] Error when editing priority and required description (#19170) * [FIX] Thread view in a channel user haven't joined (#19172) * [FIX] Livechat Appearance label and reset button (#19171) * Refactor: Omnichannel departments (#18920) Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat> Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> * Fix multi users on search * Livechat RoomMenu * Fix Header in anonymous sessions * Fix sidebar * Fix admin user Info * update base old * fix sidebar size * Fix sidebar tests * Lint * Package-lock * package-lock * Fix callback * Removed useless files * Fix LGTM * Isolate userpresence to dont leak react and fuselage * Fix Alert * update fuselage * fix hide modal not closing * Sort by name and activity * Fix reset * Arrow controls (#19239) Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> * Fixes * ActionButton * ActionButton[2] * ActionButton [3] * Support anonymous * Open menu by keyboard * Login button for anonymous users * Login button for anonymous users * Update code * ShouldUpdate * Change login Icon, fix badge * Update fuselage * Fix storybook * Fix storybook * Use Style and renamed * wip stories sidebar * Types * wip * Testing IE11 * WIP * Fix Typo * Use Layout colors * Lint * Fix * Remove CallProvider * Remove CallContext Co-authored-by: Martin <martin.schoeler@rocket.chat> Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat> Co-authored-by: Douglas Fabris <deefabris@gmail.com> Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Co-authored-by: Renato Becker <renato.augusto.becker@gmail.com> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>pull/19290/head^2
parent
c54ab5fd23
commit
edda3511c2
@ -1,12 +0,0 @@ |
||||
<template name="DiscussionList"> |
||||
{{#if shouldAppear}} |
||||
{{#if rooms}} |
||||
<h3 class="rooms-list__type"> |
||||
{{_ "Discussions"}} |
||||
</h3> |
||||
<ul class="rooms-list__list"> |
||||
{{#each room in rooms}} {{> chatRoomItem room }} {{/each}} |
||||
</ul> |
||||
{{/if}} |
||||
{{/if}} |
||||
</template> |
@ -1,33 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { ChatSubscription } from '../../../models/client'; |
||||
import { getUserPreference } from '../../../utils/client'; |
||||
import { settings } from '../../../settings/client'; |
||||
|
||||
import './DiscussionList.html'; |
||||
|
||||
Template.DiscussionList.helpers({ |
||||
rooms() { |
||||
const user = Meteor.userId(); |
||||
const sortBy = getUserPreference(user, 'sidebarSortby') || 'activity'; |
||||
const query = { |
||||
open: true, |
||||
}; |
||||
|
||||
const sort = {}; |
||||
|
||||
if (sortBy === 'activity') { |
||||
sort.lm = -1; |
||||
} else { // alphabetical
|
||||
sort[this.identifier === 'd' && settings.get('UI_Use_Real_Name') ? 'lowerCaseFName' : 'lowerCaseName'] = /descending/.test(sortBy) ? -1 : 1; |
||||
} |
||||
|
||||
query.prid = { $exists: true }; |
||||
return ChatSubscription.find(query, { sort }); |
||||
}, |
||||
|
||||
shouldAppear() { |
||||
return settings.get('Discussion_enabled'); |
||||
}, |
||||
}); |
@ -1,76 +1,88 @@ |
||||
import { APIClient } from '../../../../utils/client'; |
||||
import { LivechatInquiry } from '../../collections/LivechatInquiry'; |
||||
import { inquiryDataStream } from './inquiry'; |
||||
import { hasRole } from '../../../../authorization/client'; |
||||
import { call } from '../../../../ui-utils/client'; |
||||
|
||||
let agentDepartments = []; |
||||
const departments = new Set(); |
||||
|
||||
const events = { |
||||
added: (inquiry) => { |
||||
delete inquiry.type; |
||||
LivechatInquiry.insert(inquiry); |
||||
departments.has(inquiry.department) && LivechatInquiry.insert({ ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); |
||||
}, |
||||
changed: (inquiry) => { |
||||
if (inquiry.status !== 'queued' || (inquiry.department && !agentDepartments.includes(inquiry.department))) { |
||||
if (inquiry.status !== 'queued' || (inquiry.department && !departments.has(inquiry.department))) { |
||||
return LivechatInquiry.remove(inquiry._id); |
||||
} |
||||
delete inquiry.type; |
||||
LivechatInquiry.upsert({ _id: inquiry._id }, inquiry); |
||||
LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); |
||||
}, |
||||
removed: (inquiry) => LivechatInquiry.remove(inquiry._id), |
||||
}; |
||||
|
||||
const updateCollection = (inquiry) => { events[inquiry.type](inquiry); }; |
||||
const appendListenerToDepartment = (departmentId) => inquiryDataStream.on(`department/${ departmentId }`, updateCollection); |
||||
const removeListenerOfDepartment = (departmentId) => inquiryDataStream.removeListener(`department/${ departmentId }`, updateCollection); |
||||
|
||||
const getInquiriesFromAPI = async (url) => { |
||||
const { inquiries } = await APIClient.v1.get(url); |
||||
const getInquiriesFromAPI = async () => { |
||||
const { inquiries } = await APIClient.v1.get('livechat/inquiries.queued?sort={"ts": 1}'); |
||||
return inquiries; |
||||
}; |
||||
|
||||
const updateInquiries = async (inquiries) => { |
||||
(inquiries || []).forEach((inquiry) => LivechatInquiry.upsert({ _id: inquiry._id }, inquiry)); |
||||
const removeListenerOfDepartment = (departmentId) => { |
||||
inquiryDataStream.removeListener(`department/${ departmentId }`, updateCollection); |
||||
departments.delete(departmentId); |
||||
}; |
||||
|
||||
const appendListenerToDepartment = (departmentId) => { |
||||
departments.add(departmentId); |
||||
inquiryDataStream.on(`department/${ departmentId }`, updateCollection); |
||||
return () => removeListenerOfDepartment(departmentId); |
||||
}; |
||||
const addListenerForeachDepartment = async (departments = []) => { |
||||
const cleanupFunctions = departments.map((department) => appendListenerToDepartment(department)); |
||||
return () => cleanupFunctions.forEach((cleanup) => cleanup()); |
||||
}; |
||||
|
||||
|
||||
const updateInquiries = async (inquiries = []) => inquiries.forEach((inquiry) => LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, _updatedAt: new Date(inquiry._updatedAt) })); |
||||
|
||||
const getAgentsDepartments = async (userId) => { |
||||
const { departments } = await APIClient.v1.get(`livechat/agents/${ userId }/departments?enabledDepartmentsOnly=true`); |
||||
return departments; |
||||
}; |
||||
|
||||
const addListenerForeachDepartment = async (userId, departments) => { |
||||
if (departments && Array.isArray(departments) && departments.length) { |
||||
departments.forEach((department) => appendListenerToDepartment(department)); |
||||
} |
||||
}; |
||||
|
||||
const removeDepartmentsListeners = (departments) => { |
||||
(departments || []).forEach((department) => removeListenerOfDepartment(department._id)); |
||||
}; |
||||
const removeGlobalListener = () => inquiryDataStream.removeListener('public', updateCollection); |
||||
|
||||
const removeGlobalListener = () => { |
||||
inquiryDataStream.removeListener('public', updateCollection); |
||||
const addGlobalListener = () => { |
||||
inquiryDataStream.on('public', updateCollection); |
||||
return removeGlobalListener; |
||||
}; |
||||
|
||||
export const initializeLivechatInquiryStream = async (userId) => { |
||||
LivechatInquiry.remove({}); |
||||
|
||||
if (agentDepartments.length) { |
||||
removeDepartmentsListeners(agentDepartments); |
||||
} |
||||
removeGlobalListener(); |
||||
|
||||
const subscribe = async (userId, isManager) => { |
||||
const config = await call('livechat:getRoutingConfig'); |
||||
if (config && config.autoAssignAgent) { |
||||
return; |
||||
} |
||||
|
||||
await updateInquiries(await getInquiriesFromAPI('livechat/inquiries.queued?sort={"ts": 1}')); |
||||
const agentDepartments = (await getAgentsDepartments(userId)).map((department) => department.departmentId); |
||||
|
||||
agentDepartments = (await getAgentsDepartments(userId)).map((department) => department.departmentId); |
||||
await addListenerForeachDepartment(userId, agentDepartments); |
||||
if (agentDepartments.length === 0 || hasRole(userId, 'livechat-manager')) { |
||||
inquiryDataStream.on('public', updateCollection); |
||||
} |
||||
const cleanUp = agentDepartments.length ? await addListenerForeachDepartment(agentDepartments) : isManager && addGlobalListener(); |
||||
|
||||
updateInquiries(await getInquiriesFromAPI()); |
||||
|
||||
return () => { |
||||
LivechatInquiry.remove({}); |
||||
removeGlobalListener(); |
||||
cleanUp && cleanUp(); |
||||
departments.clear(); |
||||
}; |
||||
}; |
||||
|
||||
export const initializeLivechatInquiryStream = (() => { |
||||
let cleanUp; |
||||
|
||||
return async (...args) => { |
||||
cleanUp && cleanUp(); |
||||
cleanUp = await subscribe(...args); |
||||
}; |
||||
})(); |
||||
|
@ -1,36 +0,0 @@ |
||||
<template name="livechat"> |
||||
<div class="livechat-section"> |
||||
<h3 class="rooms-list__type {{isActive}}"> |
||||
<span class="rooms-list__type-text--livechat">{{_ "Omnichannel"}}</span> |
||||
{{#with available}} |
||||
<i class="livechat-status {{status}} {{icon}}" title="{{hint}}"></i> |
||||
{{/with}} |
||||
</h3> |
||||
|
||||
{{#if showIncomingQueue}} |
||||
{{#if isLivechatAvailable}} |
||||
<h3 class="rooms-list__type {{isActive}}"> |
||||
{{_ "Incoming_Livechats"}} |
||||
</h3> |
||||
<ul class="rooms-list__list inquiries"> |
||||
{{#each room in inquiries}} |
||||
{{> chatRoomItem room }} |
||||
{{/each}} |
||||
</ul> |
||||
{{/if}} |
||||
|
||||
<h3 class="rooms-list__type {{isActive}}"> |
||||
{{_ "Open_Livechats"}} |
||||
</h3> |
||||
{{/if}} |
||||
|
||||
<ul class="rooms-list__list"> |
||||
{{#if showQueueLink}} |
||||
{{> sidebarItem active=activeLivechatQueue pathSection="livechat-queue" icon="queue" name="Queue"}} |
||||
{{/if}} |
||||
{{#each room in rooms}} |
||||
{{> chatRoomItem room }} |
||||
{{/each}} |
||||
</ul> |
||||
</div> |
||||
</template> |
@ -1,142 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Session } from 'meteor/session'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { ChatSubscription, Users } from '../../../../models'; |
||||
import { KonchatNotification } from '../../../../ui'; |
||||
import { settings } from '../../../../settings'; |
||||
import { hasPermission } from '../../../../authorization'; |
||||
import { t, handleError, getUserPreference } from '../../../../utils'; |
||||
import { LivechatInquiry } from '../../collections/LivechatInquiry'; |
||||
import { Notifications } from '../../../../notifications/client'; |
||||
import { initializeLivechatInquiryStream } from '../../lib/stream/queueManager'; |
||||
|
||||
import './livechat.html'; |
||||
|
||||
Template.livechat.helpers({ |
||||
isActive() { |
||||
const query = { |
||||
t: 'l', |
||||
f: { $ne: true }, |
||||
open: true, |
||||
rid: Session.get('openedRoom'), |
||||
}; |
||||
|
||||
const options = { fields: { _id: 1 } }; |
||||
|
||||
if (ChatSubscription.findOne(query, options)) { |
||||
return 'active'; |
||||
} |
||||
}, |
||||
|
||||
rooms() { |
||||
const query = { |
||||
t: 'l', |
||||
open: true, |
||||
}; |
||||
|
||||
const user = Meteor.userId(); |
||||
|
||||
if (getUserPreference(user, 'sidebarShowUnread')) { |
||||
query.alert = { $ne: true }; |
||||
} |
||||
|
||||
const sortBy = getUserPreference(user, 'sidebarSortby'); |
||||
const sort = sortBy === 'activity' ? { _updatedAt: - 1 } : { fname: 1 }; |
||||
|
||||
return ChatSubscription.find(query, { sort }); |
||||
}, |
||||
|
||||
inquiries() { |
||||
const inqs = LivechatInquiry.find({ |
||||
status: 'queued', |
||||
}, { |
||||
sort: { |
||||
queueOrder: 1, |
||||
estimatedWaitingTimeQueue: 1, |
||||
estimatedServiceTimeAt: 1, |
||||
}, |
||||
limit: Template.instance().inquiriesLimit.get(), |
||||
}); |
||||
|
||||
// for notification sound
|
||||
inqs.forEach((inq) => { |
||||
KonchatNotification.newRoom(inq.rid); |
||||
}); |
||||
|
||||
return inqs; |
||||
}, |
||||
|
||||
showIncomingQueue() { |
||||
const config = Template.instance().routingConfig.get(); |
||||
return config.showQueue; |
||||
}, |
||||
|
||||
available() { |
||||
const statusLivechat = Template.instance().statusLivechat.get(); |
||||
|
||||
return { |
||||
status: statusLivechat === 'available' ? 'status-online' : '', |
||||
icon: statusLivechat === 'available' ? 'icon-toggle-on' : 'icon-toggle-off', |
||||
hint: statusLivechat === 'available' ? t('Available') : t('Not_Available'), |
||||
}; |
||||
}, |
||||
|
||||
isLivechatAvailable() { |
||||
return Template.instance().statusLivechat.get() === 'available'; |
||||
}, |
||||
|
||||
showQueueLink() { |
||||
const config = Template.instance().routingConfig.get(); |
||||
if (!config.showQueueLink) { |
||||
return false; |
||||
} |
||||
return hasPermission(Meteor.userId(), 'view-livechat-queue') || (Template.instance().statusLivechat.get() === 'available' && settings.get('Livechat_show_queue_list_link')); |
||||
}, |
||||
|
||||
activeLivechatQueue() { |
||||
FlowRouter.watchPathChange(); |
||||
if (FlowRouter.current().route.name === 'livechat-queue') { |
||||
return 'active'; |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
Template.livechat.events({ |
||||
'click .livechat-status'() { |
||||
Meteor.call('livechat:changeLivechatStatus', (err /* , results*/) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
Template.livechat.onCreated(function() { |
||||
this.statusLivechat = new ReactiveVar(); |
||||
this.routingConfig = new ReactiveVar({}); |
||||
this.inquiriesLimit = new ReactiveVar(); |
||||
|
||||
Meteor.call('livechat:getRoutingConfig', (err, config) => { |
||||
if (config) { |
||||
this.routingConfig.set(config); |
||||
} |
||||
}); |
||||
|
||||
this.autorun(() => { |
||||
if (Meteor.userId()) { |
||||
const user = Users.findOne(Meteor.userId(), { fields: { statusLivechat: 1 } }); |
||||
this.statusLivechat.set(user.statusLivechat); |
||||
} else { |
||||
this.statusLivechat.set(); |
||||
} |
||||
}); |
||||
|
||||
initializeLivechatInquiryStream(Meteor.userId()); |
||||
this.updateAgentDepartments = () => initializeLivechatInquiryStream(Meteor.userId()); |
||||
this.autorun(() => this.inquiriesLimit.set(settings.get('Livechat_guest_pool_max_number_incoming_livechats_displayed'))); |
||||
|
||||
Notifications.onUser('departmentAgentData', (payload) => this.updateAgentDepartments(payload)); |
||||
}); |
@ -1,151 +0,0 @@ |
||||
.sidebar__header { |
||||
position: relative; |
||||
|
||||
display: flex; |
||||
|
||||
margin: 0 -10px; |
||||
padding: var(--sidebar-default-padding); |
||||
align-items: center; |
||||
|
||||
&-thumb { |
||||
position: relative; |
||||
|
||||
flex: 0 0 var(--sidebar-account-thumb-size); |
||||
|
||||
width: var(--sidebar-account-thumb-size); |
||||
height: var(--sidebar-account-thumb-size); |
||||
margin: 0 10px; |
||||
|
||||
& .avatar { |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
|
||||
&-status-bullet { |
||||
position: absolute; |
||||
|
||||
right: -2px; |
||||
bottom: -1px; |
||||
|
||||
display: block; |
||||
|
||||
width: var(--sidebar-account-status-bullet-size); |
||||
height: var(--sidebar-account-status-bullet-size); |
||||
|
||||
pointer-events: none; |
||||
|
||||
border-width: 2px; |
||||
border-style: solid; |
||||
border-color: var(--sidebar-background); |
||||
border-radius: var(--sidebar-account-status-bullet-radius); |
||||
|
||||
&--online { |
||||
background-color: var(--rc-status-online); |
||||
} |
||||
|
||||
&--away { |
||||
background-color: var(--rc-status-away); |
||||
} |
||||
|
||||
&--busy { |
||||
background-color: var(--rc-status-busy); |
||||
} |
||||
|
||||
&--invisible { |
||||
background-color: var(--rc-status-invisible); |
||||
} |
||||
|
||||
&--offline { |
||||
background-color: var(--rc-status-invisible); |
||||
} |
||||
} |
||||
} |
||||
|
||||
.sidebar__toolbar { |
||||
display: flex; |
||||
flex: 1 1 100%; |
||||
|
||||
margin: 0 -10px; |
||||
|
||||
padding: 0 10px; |
||||
|
||||
justify-content: space-between; |
||||
|
||||
&-button { |
||||
color: var(--sidebar-item-text-color); |
||||
|
||||
font-size: 20px; |
||||
fill: var(--sidebar-item-text-color); |
||||
} |
||||
|
||||
&-search { |
||||
position: absolute; |
||||
right: calc(10px + var(--sidebar-default-padding)); |
||||
|
||||
display: none; |
||||
|
||||
width: 200px; |
||||
|
||||
& .rc-input__element { |
||||
background-color: var(--sidebar-background); |
||||
} |
||||
|
||||
& .rc-input__icon { |
||||
color: white; |
||||
|
||||
&--cross { |
||||
right: 0; |
||||
left: auto; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.rc-popover--sidebar-header { |
||||
& .rc-popover__icon-element--circle { |
||||
font-size: var(--sidebar-account-status-bullet-size); |
||||
} |
||||
|
||||
& .rc-popover__item { |
||||
&--online { |
||||
& .rc-icon { |
||||
color: var(--rc-status-online); |
||||
} |
||||
} |
||||
|
||||
&--away { |
||||
& .rc-icon { |
||||
color: var(--rc-status-away); |
||||
} |
||||
} |
||||
|
||||
&--busy { |
||||
& .rc-icon { |
||||
color: var(--rc-status-busy); |
||||
} |
||||
} |
||||
|
||||
&--offline { |
||||
& .rc-icon { |
||||
color: var(--rc-status-invisible); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@media (min-width: 1372px) { /* 1440px -68px (eletron menu) */ |
||||
.sidebar { |
||||
flex: 0 0 20%; |
||||
|
||||
width: 20%; |
||||
max-width: 20%; |
||||
|
||||
&__toolbar { |
||||
justify-content: flex-end; |
||||
|
||||
&-button { |
||||
margin: 0 6px; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,334 +0,0 @@ |
||||
.sidebar-light .sidebar-item { |
||||
color: var(--color-dark); |
||||
|
||||
&:hover { |
||||
background-color: var(--sidebar-background-light-hover); |
||||
} |
||||
|
||||
&--active { |
||||
background-color: var(--sidebar-background-light-active); |
||||
} |
||||
|
||||
&__picture { |
||||
color: inherit; |
||||
} |
||||
|
||||
&__message { |
||||
margin: 0; |
||||
} |
||||
} |
||||
|
||||
.sidebar--hide-avatar .sidebar-item__picture { |
||||
display: none; |
||||
} |
||||
|
||||
.sidebar--extended .sidebar-item { |
||||
height: var(--sidebar-item-height-extended); |
||||
|
||||
&__picture { |
||||
flex: 0 0 var(--sidebar-item-thumb-size-extended); |
||||
|
||||
width: var(--sidebar-item-thumb-size-extended); |
||||
height: var(--sidebar-item-thumb-size-extended); |
||||
} |
||||
|
||||
&__user-thumb { |
||||
width: var(--sidebar-item-thumb-size-extended); |
||||
height: var(--sidebar-item-thumb-size-extended); |
||||
} |
||||
|
||||
&__message { |
||||
flex-direction: column; |
||||
|
||||
height: var(--sidebar-item-thumb-size-extended); |
||||
|
||||
&-top, |
||||
&-bottom { |
||||
display: flex; |
||||
|
||||
width: 100%; |
||||
align-items: center; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.sidebar--medium .sidebar-item { |
||||
height: var(--sidebar-item-height-medium); |
||||
|
||||
&__picture { |
||||
flex: 0 0 var(--sidebar-item-thumb-size-medium); |
||||
|
||||
width: var(--sidebar-item-thumb-size-medium); |
||||
height: var(--sidebar-item-thumb-size-medium); |
||||
} |
||||
|
||||
&__user-thumb { |
||||
width: var(--sidebar-item-thumb-size-medium); |
||||
height: var(--sidebar-item-thumb-size-medium); |
||||
} |
||||
} |
||||
|
||||
.sidebar-item { |
||||
position: relative; |
||||
|
||||
display: flex; |
||||
|
||||
height: var(--sidebar-item-height); |
||||
|
||||
padding: 0 var(--sidebar-default-padding); |
||||
|
||||
cursor: pointer; |
||||
|
||||
transition: all 0.3s; |
||||
|
||||
color: var(--sidebar-item-text-color); |
||||
|
||||
border-radius: var(--sidebar-item-radius); |
||||
|
||||
background-color: var(--sidebar-item-background); |
||||
|
||||
align-items: stretch; |
||||
|
||||
&:hover { |
||||
background-color: var(--sidebar-item-hover-background); |
||||
|
||||
& .sidebar-item__menu { |
||||
display: flex; |
||||
} |
||||
} |
||||
|
||||
&--active { |
||||
background-color: var(--sidebar-item-active-background); |
||||
} |
||||
|
||||
&--unread &__message-top, |
||||
&--mention &__message-top { |
||||
color: var(--sidebar-item-unread-color); |
||||
|
||||
font-weight: var(--sidebar-item-unread-font-weight); |
||||
} |
||||
|
||||
&__popup-active { |
||||
background-color: var(--sidebar-item-popup-background); |
||||
} |
||||
|
||||
&__link { |
||||
display: flex; |
||||
overflow: hidden; |
||||
flex: 1; |
||||
|
||||
margin: 0 -2px; |
||||
|
||||
color: inherit; |
||||
|
||||
font-size: 1rem; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__icon { |
||||
|
||||
display: flex; |
||||
|
||||
width: 20px; |
||||
|
||||
font-size: 1rem; |
||||
|
||||
align-items: center; |
||||
|
||||
&-status { |
||||
&--online { |
||||
color: var(--rc-status-online); |
||||
} |
||||
|
||||
&--away { |
||||
color: var(--rc-status-away); |
||||
} |
||||
|
||||
&--busy { |
||||
color: var(--rc-status-busy); |
||||
} |
||||
} |
||||
} |
||||
|
||||
&__video { |
||||
|
||||
margin: 0 2px; |
||||
|
||||
color: var(--rc-color-success); |
||||
|
||||
font-size: 1rem; |
||||
} |
||||
|
||||
&__user-thumb { |
||||
width: var(--sidebar-item-thumb-size); |
||||
height: var(--sidebar-item-thumb-size); |
||||
} |
||||
|
||||
&__user-status { |
||||
flex: 0 0 auto; |
||||
|
||||
width: var(--sidebar-item-user-status-size); |
||||
height: var(--sidebar-item-user-status-size); |
||||
|
||||
margin: 0 7px; |
||||
|
||||
border-radius: var(--sidebar-item-user-status-radius); |
||||
|
||||
&--online { |
||||
background-color: var(--rc-status-online); |
||||
} |
||||
|
||||
&--away { |
||||
background-color: var(--rc-status-away); |
||||
} |
||||
|
||||
&--busy { |
||||
background-color: var(--rc-status-busy); |
||||
} |
||||
|
||||
&--offline { |
||||
background-color: var(--rc-status-invisible-sidebar); |
||||
} |
||||
} |
||||
|
||||
&__picture { |
||||
display: flex; |
||||
flex: 0 0 var(--sidebar-item-thumb-size); |
||||
|
||||
height: 20px; |
||||
|
||||
margin: 0 2px; |
||||
|
||||
color: var(--sidebar-item-unread-color); |
||||
border-radius: var(--sidebar-item-radius); |
||||
|
||||
align-items: center; |
||||
justify-content: center; |
||||
} |
||||
|
||||
&__body { |
||||
display: flex; |
||||
|
||||
overflow: hidden; |
||||
flex: 1; |
||||
|
||||
margin: 0 4px; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__message { |
||||
display: flex; |
||||
overflow: hidden; |
||||
flex: 1; |
||||
|
||||
margin: 0 -3px; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
|
||||
&-top { |
||||
overflow: hidden; |
||||
|
||||
width: 100%; |
||||
} |
||||
} |
||||
|
||||
&__ellipsis { |
||||
overflow: hidden; |
||||
|
||||
flex: 1; |
||||
|
||||
margin: 0 2px; |
||||
|
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
&__name { |
||||
display: flex; |
||||
|
||||
overflow: hidden; |
||||
|
||||
flex: 1; |
||||
|
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
|
||||
font-size: var(--sidebar-item-text-size); |
||||
|
||||
line-height: 1.2rem; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__me { |
||||
text-transform: lowercase; |
||||
} |
||||
|
||||
&__last-message { |
||||
overflow: hidden; |
||||
flex: 1; |
||||
|
||||
margin: 0 5px; |
||||
|
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
|
||||
font-size: 12px; |
||||
line-height: normal; |
||||
|
||||
&--unread { |
||||
color: var(--sidebar-item-unread-color); |
||||
|
||||
font-weight: var(--sidebar-item-unread-font-weight); |
||||
} |
||||
} |
||||
|
||||
&__time { |
||||
margin: 0 3px; |
||||
|
||||
color: var(--sidebar-item-text-color); |
||||
|
||||
font-size: 10px; |
||||
} |
||||
|
||||
&__menu { |
||||
position: absolute; |
||||
|
||||
top: 0; |
||||
|
||||
right: 0; |
||||
|
||||
display: none; |
||||
flex: 0; |
||||
|
||||
height: 100%; |
||||
|
||||
padding: 6px; |
||||
align-items: center; |
||||
justify-content: center; |
||||
|
||||
&-icon { |
||||
fill: var(--color-white); |
||||
} |
||||
} |
||||
} |
||||
|
||||
.flex-nav .sidebar-item__message { |
||||
flex-direction: row; |
||||
} |
||||
|
||||
.rtl .sidebar-item { |
||||
&__menu { |
||||
right: auto; |
||||
left: 0; |
||||
} |
||||
} |
||||
|
||||
@media (width <= 400px) { |
||||
.sidebar-item { |
||||
padding: 0 0 0 var(--sidebar-small-default-padding); |
||||
} |
||||
|
||||
.rtl .sidebar-item { |
||||
padding: 0 var(--sidebar-small-default-padding) 0 0; |
||||
} |
||||
} |
@ -1,123 +0,0 @@ |
||||
.toolbar { |
||||
position: absolute; |
||||
left: 10px; |
||||
|
||||
width: 100%; |
||||
margin: 0 -10px; |
||||
padding: 0 calc(var(--sidebar-default-padding) + 10px); |
||||
|
||||
&__wrapper { |
||||
|
||||
display: flex; |
||||
|
||||
margin: 0 -0.25rem; |
||||
|
||||
color: var(--toolbar-placeholder-color); |
||||
} |
||||
|
||||
&__search { |
||||
position: relative; |
||||
|
||||
display: flex; |
||||
|
||||
width: 100%; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__search-input { |
||||
width: 100%; |
||||
padding-left: 32px; |
||||
|
||||
&:focus + svg { |
||||
display: block; |
||||
} |
||||
} |
||||
|
||||
&__search-buttons { |
||||
margin-left: 8px; |
||||
} |
||||
|
||||
&__icon { |
||||
&--plus { |
||||
position: absolute; |
||||
top: 50%; |
||||
left: 50%; |
||||
|
||||
transform: translate(-50%, -50%); |
||||
|
||||
font-size: 1.25rem; |
||||
} |
||||
} |
||||
|
||||
& .rc-input { |
||||
margin: 0 0.25rem; |
||||
|
||||
&__wrapper { |
||||
padding: 0; |
||||
|
||||
color: var(--rc-color-primary-light); |
||||
} |
||||
|
||||
&__element { |
||||
color: var(--color-white); |
||||
border-color: var(--rc-color-primary-dark); |
||||
background-color: var(--rc-color-primary-darkest); |
||||
|
||||
&::placeholder { |
||||
color: var(--rc-color-primary-light); |
||||
} |
||||
|
||||
&:focus + .rc-input__icon--right { |
||||
display: flex; |
||||
} |
||||
} |
||||
|
||||
&__icon { |
||||
left: 0.5rem; |
||||
fill: var(--rc-color-primary-light); |
||||
|
||||
&--right { |
||||
right: 0.5rem; |
||||
left: auto; |
||||
|
||||
display: none; |
||||
} |
||||
|
||||
& + .rc-input__element { |
||||
padding: 0.5rem 1.5rem 0.5rem 2.25rem; |
||||
} |
||||
|
||||
&-svg--plus { |
||||
transform: rotate(45deg); |
||||
|
||||
font-size: 1rem; |
||||
} |
||||
} |
||||
} |
||||
|
||||
& .rc-button { |
||||
|
||||
min-height: 36px; |
||||
margin: 0 0.25rem; |
||||
|
||||
color: var(--rc-color-primary-light); |
||||
border-color: var(--rc-color-primary-dark); |
||||
background-color: var(--rc-color-primary-darkest); |
||||
} |
||||
|
||||
& .rc-input__icon-svg--magnifier { |
||||
font-size: 1rem; |
||||
} |
||||
} |
||||
|
||||
@media (width <= 400px) { |
||||
.toolbar { |
||||
padding: 0 var(--sidebar-extra-small-default-padding) var(--sidebar-small-default-padding); |
||||
} |
||||
} |
||||
|
||||
.rtl .toolbar { |
||||
& .rc-input__icon + .rc-input__element { |
||||
padding: 0.5rem 2.25rem 0.5rem 1rem; |
||||
} |
||||
} |
@ -1,3 +0,0 @@ |
||||
<template name="chatRoomItem"> |
||||
{{> sidebarItem roomData room }} |
||||
</template> |
@ -1,53 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { t, roomTypes } from '../../utils/client'; |
||||
import { settings } from '../../settings/client'; |
||||
import { Rooms } from '../../models/client'; |
||||
import { callbacks } from '../../callbacks/client'; |
||||
|
||||
Template.chatRoomItem.helpers({ |
||||
roomData() { |
||||
const unread = this.unread > 0 ? this.unread : false; |
||||
// if (this.unread > 0 && (!hasFocus || openedRoom !== this.rid)) {
|
||||
// unread = this.unread;
|
||||
// }
|
||||
|
||||
const roomType = roomTypes.getConfig(this.t); |
||||
|
||||
const archivedClass = this.archived ? 'archived' : false; |
||||
|
||||
const room = Rooms.findOne(this.rid); |
||||
|
||||
const icon = roomTypes.getIcon(this.t === 'd' ? room : this); |
||||
|
||||
const roomData = { |
||||
...this, |
||||
icon: icon !== 'at' && icon, |
||||
avatar: roomTypes.getConfig(this.t).getAvatarPath(room || this), |
||||
username: this.name, |
||||
route: roomTypes.getRouteLink(this.t, this), |
||||
name: roomType.roomName(this), |
||||
unread, |
||||
active: false, |
||||
archivedClass, |
||||
status: this.t === 'd' || this.t === 'l', |
||||
isGroupChat: roomType.isGroupChat(room), |
||||
}; |
||||
roomData.username = roomData.username || roomData.name; |
||||
|
||||
if (!this.lastMessage && settings.get('Store_Last_Message')) { |
||||
const room = Rooms.findOne(this.rid || this._id, { fields: { lastMessage: 1 } }); |
||||
roomData.lastMessage = (room && room.lastMessage) || { msg: t('No_messages_yet') }; |
||||
} |
||||
return roomData; |
||||
}, |
||||
}); |
||||
|
||||
callbacks.add('enter-room', (sub) => { |
||||
const items = $('.rooms-list .sidebar-item'); |
||||
items.filter('.sidebar-item--active').removeClass('sidebar-item--active'); |
||||
if (sub) { |
||||
items.filter(`[data-id=${ sub._id }]`).addClass('sidebar-item--active'); |
||||
} |
||||
return sub; |
||||
}); |
@ -1,20 +0,0 @@ |
||||
<template name="sidebarHeader"> |
||||
<header class="sidebar__header"> |
||||
{{#with myUserInfo}} |
||||
<div aria-haspopup="true" class="sidebar__header-thumb"> |
||||
{{> avatar username=username}} |
||||
<div class="sidebar__header-status-bullet sidebar__header-status-bullet--{{status}}"></div> |
||||
</div> |
||||
<div class="sidebar__toolbar"> |
||||
{{#each toolbarButtons}} |
||||
<button class="sidebar__toolbar-button js-button" title="{{name}}" aria-haspopup="{{hasPopup}}"> |
||||
{{> icon block="sidebar__toolbar-button-icon" icon=icon }} |
||||
</button> |
||||
{{/each}} |
||||
</div> |
||||
{{/with}} |
||||
{{#if showToolbar}} |
||||
{{> toolbar }} |
||||
{{/if}} |
||||
</header> |
||||
</template> |
@ -1,344 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { popover, AccountBox, menu, SideNav, modal } from '../../ui-utils'; |
||||
import { t } from '../../utils'; |
||||
import { callbacks } from '../../callbacks'; |
||||
import { settings } from '../../settings'; |
||||
import { hasAtLeastOnePermission } from '../../authorization'; |
||||
import { userStatus } from '../../user-status'; |
||||
import { hasPermission } from '../../authorization/client'; |
||||
import { createTemplateForComponent } from '../../../client/reactAdapters'; |
||||
|
||||
|
||||
const setStatus = (status, statusText) => { |
||||
AccountBox.setStatus(status, statusText); |
||||
callbacks.run('userStatusManuallySet', status); |
||||
popover.close(); |
||||
}; |
||||
|
||||
const showToolbar = new ReactiveVar(false); |
||||
|
||||
export const toolbarSearch = { |
||||
shortcut: false, |
||||
show(fromShortcut) { |
||||
menu.open(); |
||||
showToolbar.set(true); |
||||
this.shortcut = fromShortcut; |
||||
}, |
||||
close() { |
||||
showToolbar.set(false); |
||||
if (this.shortcut) { |
||||
menu.close(); |
||||
} |
||||
}, |
||||
}; |
||||
|
||||
const toolbarButtons = (/* user */) => [{ |
||||
name: t('Home'), |
||||
icon: 'home', |
||||
condition: () => settings.get('Layout_Show_Home_Button'), |
||||
action: () => { |
||||
FlowRouter.go('home'); |
||||
}, |
||||
}, |
||||
{ |
||||
name: t('Search'), |
||||
icon: 'magnifier', |
||||
action: () => { |
||||
toolbarSearch.show(false); |
||||
}, |
||||
}, |
||||
{ |
||||
name: t('Directory'), |
||||
icon: 'discover', |
||||
action: () => { |
||||
menu.close(); |
||||
FlowRouter.go('directory'); |
||||
}, |
||||
}, |
||||
{ |
||||
name: t('Sort'), |
||||
icon: 'sort', |
||||
hasPopup: true, |
||||
action: async (e) => { |
||||
const options = []; |
||||
const config = { |
||||
template: createTemplateForComponent('SortList', () => import('../../../client/components/SortList')), |
||||
currentTarget: e.currentTarget, |
||||
data: { |
||||
options, |
||||
}, |
||||
offsetVertical: e.currentTarget.clientHeight + 10, |
||||
}; |
||||
popover.open(config); |
||||
}, |
||||
}, |
||||
{ |
||||
name: t('Create_new'), |
||||
icon: 'edit-rounded', |
||||
condition: () => hasAtLeastOnePermission(['create-c', 'create-p', 'create-d', 'start-discussion', 'start-discussion-other-user']), |
||||
hasPopup: true, |
||||
action: (e) => { |
||||
const action = (title, content) => (e) => { |
||||
e.preventDefault(); |
||||
modal.open({ |
||||
title: t(title), |
||||
content, |
||||
data: { |
||||
onCreate() { |
||||
modal.close(); |
||||
}, |
||||
}, |
||||
modifier: 'modal', |
||||
showConfirmButton: false, |
||||
showCancelButton: false, |
||||
confirmOnEnter: false, |
||||
}); |
||||
}; |
||||
|
||||
const createChannel = action('Create_A_New_Channel', 'createChannel'); |
||||
const createDirectMessage = action('Direct_Messages', 'CreateDirectMessage'); |
||||
const createDiscussion = action('Discussion_title', 'CreateDiscussion'); |
||||
|
||||
|
||||
const items = [ |
||||
hasAtLeastOnePermission(['create-c', 'create-p']) |
||||
&& { |
||||
icon: 'hashtag', |
||||
name: t('Channel'), |
||||
action: createChannel, |
||||
}, |
||||
hasPermission('create-d') |
||||
&& { |
||||
icon: 'team', |
||||
name: t('Direct_Messages'), |
||||
action: createDirectMessage, |
||||
}, |
||||
settings.get('Discussion_enabled') && hasAtLeastOnePermission(['start-discussion', 'start-discussion-other-user']) |
||||
&& { |
||||
icon: 'discussion', |
||||
name: t('Discussion'), |
||||
action: createDiscussion, |
||||
}, |
||||
].filter(Boolean); |
||||
|
||||
if (items.length === 1) { |
||||
return items[0].action(e); |
||||
} |
||||
|
||||
const config = { |
||||
columns: [ |
||||
{ |
||||
groups: [ |
||||
{ |
||||
items, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
currentTarget: e.currentTarget, |
||||
offsetVertical: e.currentTarget.clientHeight + 10, |
||||
}; |
||||
popover.open(config); |
||||
}, |
||||
}, |
||||
{ |
||||
name: t('Options'), |
||||
icon: 'menu', |
||||
condition: () => AccountBox.getItems().length || hasAtLeastOnePermission(['manage-emoji', 'manage-oauth-apps', 'manage-outgoing-integrations', 'manage-incoming-integrations', 'manage-own-outgoing-integrations', 'manage-own-incoming-integrations', 'manage-selected-settings', 'manage-sounds', 'view-logs', 'view-privileged-setting', 'view-room-administration', 'view-statistics', 'view-user-administration', 'access-setting-permissions']), |
||||
hasPopup: true, |
||||
action: (e) => { |
||||
let adminOption; |
||||
if (hasAtLeastOnePermission(['manage-emoji', 'manage-oauth-apps', 'manage-outgoing-integrations', 'manage-incoming-integrations', 'manage-own-outgoing-integrations', 'manage-own-incoming-integrations', 'manage-selected-settings', 'manage-sounds', 'view-logs', 'view-privileged-setting', 'view-room-administration', 'view-statistics', 'view-user-administration', 'access-setting-permissions'])) { |
||||
adminOption = { |
||||
icon: 'customize', |
||||
name: t('Administration'), |
||||
type: 'open', |
||||
id: 'administration', |
||||
action: () => { |
||||
FlowRouter.go('admin', { group: 'info' }); |
||||
popover.close(); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
const config = { |
||||
popoverClass: 'sidebar-header', |
||||
columns: [ |
||||
{ |
||||
groups: [ |
||||
{ |
||||
items: AccountBox.getItems().map((item) => { |
||||
let action; |
||||
|
||||
if (item.href || item.sideNav) { |
||||
action = () => { |
||||
if (item.href) { |
||||
FlowRouter.go(item.href); |
||||
popover.close(); |
||||
} |
||||
if (item.sideNav) { |
||||
SideNav.setFlex(item.sideNav); |
||||
SideNav.openFlex(); |
||||
popover.close(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
icon: item.icon, |
||||
name: t(item.name), |
||||
type: 'open', |
||||
id: item.name, |
||||
href: item.href, |
||||
sideNav: item.sideNav, |
||||
action, |
||||
}; |
||||
}).concat([adminOption]), |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
currentTarget: e.currentTarget, |
||||
offsetVertical: e.currentTarget.clientHeight + 10, |
||||
}; |
||||
|
||||
popover.open(config); |
||||
}, |
||||
}]; |
||||
Template.sidebarHeader.helpers({ |
||||
myUserInfo() { |
||||
const id = Meteor.userId(); |
||||
|
||||
if (id == null && settings.get('Accounts_AllowAnonymousRead')) { |
||||
return { |
||||
username: 'anonymous', |
||||
status: 'online', |
||||
}; |
||||
} |
||||
return id && Meteor.users.findOne(id, { fields: { |
||||
username: 1, status: 1, statusText: 1, |
||||
} }); |
||||
}, |
||||
toolbarButtons() { |
||||
return toolbarButtons(/* Meteor.userId() */).filter((button) => !button.condition || button.condition()); |
||||
}, |
||||
showToolbar() { |
||||
return showToolbar.get(); |
||||
}, |
||||
}); |
||||
|
||||
Template.sidebarHeader.events({ |
||||
'click .js-button'(e) { |
||||
if (document.activeElement === e.currentTarget) { |
||||
e.currentTarget.blur(); |
||||
} |
||||
return this.action && this.action.apply(this, [e]); |
||||
}, |
||||
'click .sidebar__header .avatar'(e) { |
||||
if (!(Meteor.userId() == null && settings.get('Accounts_AllowAnonymousRead'))) { |
||||
const user = Meteor.user(); |
||||
const STATUS_MAP = [ |
||||
'offline', |
||||
'online', |
||||
'away', |
||||
'busy', |
||||
]; |
||||
const userStatusList = Object.keys(userStatus.list).map((key) => { |
||||
const status = userStatus.list[key]; |
||||
const name = status.localizeName ? t(status.name) : status.name; |
||||
const modifier = status.statusType || user.status; |
||||
const defaultStatus = STATUS_MAP.includes(status.id); |
||||
const statusText = defaultStatus ? null : name; |
||||
|
||||
return { |
||||
icon: 'circle', |
||||
name, |
||||
modifier, |
||||
action: () => setStatus(status.statusType, statusText), |
||||
}; |
||||
}); |
||||
|
||||
const statusText = user.statusText || t(user.status); |
||||
|
||||
userStatusList.push({ |
||||
icon: 'edit', |
||||
name: t('Edit_Status'), |
||||
type: 'open', |
||||
action: (e) => { |
||||
e.preventDefault(); |
||||
modal.open({ |
||||
title: t('Edit_Status'), |
||||
content: 'editStatus', |
||||
data: { |
||||
onSave() { |
||||
modal.close(); |
||||
}, |
||||
}, |
||||
modalClass: 'modal', |
||||
showConfirmButton: false, |
||||
showCancelButton: false, |
||||
confirmOnEnter: false, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
const config = { |
||||
popoverClass: 'sidebar-header', |
||||
columns: [ |
||||
{ |
||||
groups: [ |
||||
{ |
||||
title: user.name, |
||||
items: [{ |
||||
icon: 'circle', |
||||
name: statusText, |
||||
modifier: user.status, |
||||
}], |
||||
}, |
||||
{ |
||||
title: t('User'), |
||||
items: userStatusList, |
||||
}, |
||||
{ |
||||
items: [ |
||||
{ |
||||
icon: 'user', |
||||
name: t('My_Account'), |
||||
type: 'open', |
||||
id: 'account', |
||||
action: () => { |
||||
FlowRouter.go('account'); |
||||
popover.close(); |
||||
}, |
||||
}, |
||||
{ |
||||
icon: 'sign-out', |
||||
name: t('Logout'), |
||||
type: 'open', |
||||
id: 'logout', |
||||
action: () => { |
||||
Meteor.logout(() => { |
||||
callbacks.run('afterLogoutCleanUp', user); |
||||
Meteor.call('logoutCleanUp', user); |
||||
FlowRouter.go('home'); |
||||
popover.close(); |
||||
}); |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
currentTarget: e.currentTarget, |
||||
offsetVertical: e.currentTarget.clientHeight + 10, |
||||
}; |
||||
|
||||
popover.open(config); |
||||
} |
||||
}, |
||||
}); |
@ -1,76 +0,0 @@ |
||||
<template name="sidebarItemIcon"> |
||||
{{#if icon}} |
||||
<div class="{{#if isRoom}}sidebar-item__room-type{{else}}sidebar-item__icon{{/if}} {{#if status}}sidebar-item__icon-status--{{status}}{{/if}}"> |
||||
{{> icon block="rc-icon--default-size sidebar-item__icon sidebar-item__icon" icon=icon}} |
||||
</div> |
||||
{{else}} |
||||
{{# userPresence uid=uid}}<div class="sidebar-item__user-status {{#if status}}sidebar-item__user-status--{{status}}{{/if}}" aria-label="{{status}}"></div>{{/userPresence}} |
||||
{{/if}} |
||||
</template> |
||||
|
||||
<template name="sidebarItem"> |
||||
<li class="sidebar-item{{#if showUnread }} sidebar-item--unread{{/if}}{{#if active}} sidebar-item--active{{/if}}{{#if toolbar}} popup-item{{/if}} js-sidebar-type-{{t}}" data-id="{{_id}}"> |
||||
<a class="sidebar-item__link" href="{{#if route}}{{route}}{{else}}{{pathFor pathSection group=pathGroup}}{{/if}}" aria-label="{{name}}"> |
||||
{{#unless isLivechatQueue}} |
||||
|
||||
<div class=" {{#if isRoom}}sidebar-item__picture{{else}}sidebar-item-option-icon{{/if}}"> |
||||
{{#if darken}} |
||||
{{#if icon}} |
||||
<div class="{{#if isRoom}}sidebar-item__room-type{{else}}sidebar-item__icon{{/if}}"> |
||||
{{> icon block="sidebar-item__icon rc-icon--default-size" icon=icon}} |
||||
</div> |
||||
{{/if}} |
||||
{{else}} |
||||
<div class="sidebar-item__user-thumb"> |
||||
{{> avatar url=avatar roomIcon=icon lazy=true}} |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
{{/unless}} |
||||
<div class="sidebar-item__body"> |
||||
{{# let extended=isExtendedViewMode}} |
||||
<div class="sidebar-item__message"> |
||||
<div class="sidebar-item__message-top"> |
||||
<div class="sidebar-item__name"> |
||||
{{#unless darken}} |
||||
{{> sidebarItemIcon}} |
||||
{{/unless}} |
||||
|
||||
<div class="sidebar-item__ellipsis"> |
||||
{{name}} |
||||
{{#if mySelf}} |
||||
<span class="sidebar-item__me">({{_ "You"}})</span> |
||||
{{/if}} |
||||
</div> |
||||
{{#if streaming}} {{>icon icon="video" block="sidebar-item__video pulse"}} {{/if}} |
||||
</div> |
||||
{{#if extended}} |
||||
{{#if lastMessageTs}} |
||||
<span class="sidebar-item__time">{{lastMessageTs}}</span> |
||||
{{/if}} |
||||
{{/if}} |
||||
</div> |
||||
<div class="sidebar-item__message-bottom"> |
||||
{{#if extended}} |
||||
{{#if lastMessage}} |
||||
<div class="sidebar-item__last-message {{#if showUnread }}{{#if lastMessageUnread }} sidebar-item__last-message--unread{{/if}}{{/if}}"> |
||||
<span class="message-body--unstyled">{{{lastMessage}}}</span> |
||||
</div> |
||||
{{/if}} |
||||
{{/if}} |
||||
{{#if unread}} |
||||
<span class="{{badgeClass}}">{{unread}}</span> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
{{/let}} |
||||
|
||||
{{#if isRoom}} |
||||
<div class="sidebar-item__menu" aria-haspopup="true"> |
||||
{{> icon block="sidebar-item__menu-icon rc-icon--default-size" icon="menu"}} |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
</a> |
||||
</li> |
||||
</template> |
@ -1,238 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Session } from 'meteor/session'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { t, getUserPreference, roomTypes } from '../../utils'; |
||||
import { popover, renderMessageBody, menu } from '../../ui-utils'; |
||||
import { Users, ChatSubscription } from '../../models/client'; |
||||
import { settings } from '../../settings'; |
||||
import { hasAtLeastOnePermission } from '../../authorization'; |
||||
import { timeAgo } from '../../lib/client/lib/formatDate'; |
||||
import { getUidDirectMessage } from '../../ui-utils/client/lib/getUidDirectMessage'; |
||||
|
||||
Template.sidebarItem.helpers({ |
||||
streaming() { |
||||
return this.streamingOptions && Object.keys(this.streamingOptions).length; |
||||
}, |
||||
isRoom() { |
||||
return this.rid || this._id; |
||||
}, |
||||
isExtendedViewMode() { |
||||
return getUserPreference(Meteor.userId(), 'sidebarViewMode') === 'extended'; |
||||
}, |
||||
lastMessage() { |
||||
return this.lastMessage && Template.instance().renderedMessage; |
||||
}, |
||||
lastMessageTs() { |
||||
return this.lastMessage && Template.instance().lastMessageTs.get(); |
||||
}, |
||||
mySelf() { |
||||
return this.t === 'd' && this.name === Template.instance().user.username; |
||||
}, |
||||
isLivechatQueue() { |
||||
return this.pathSection === 'livechat-queue'; |
||||
}, |
||||
showUnread() { |
||||
return this.unread > 0 || (!this.hideUnreadStatus && this.alert); |
||||
}, |
||||
unread() { |
||||
const { unread = 0, tunread = [] } = this; |
||||
return unread + tunread.length; |
||||
}, |
||||
lastMessageUnread() { |
||||
if (!this.ls) { |
||||
return true; |
||||
} |
||||
if (!this.lastMessage?.ts) { |
||||
return false; |
||||
} |
||||
|
||||
return this.lastMessage.ts > this.ls; |
||||
}, |
||||
badgeClass() { |
||||
const { unread, userMentions, groupMentions, tunread = [], tunreadGroup = [], tunreadUser = [] } = this; |
||||
|
||||
if (userMentions || tunreadUser.length > 0) { |
||||
return 'badge badge--user-mentions'; |
||||
} |
||||
|
||||
if (groupMentions || tunreadGroup.length > 0) { |
||||
return 'badge badge--group-mentions'; |
||||
} |
||||
|
||||
if (tunread.length) { |
||||
return 'badge badge--thread'; |
||||
} |
||||
|
||||
if (unread) { |
||||
return 'badge'; |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
function setLastMessageTs(instance, ts) { |
||||
if (instance.timeAgoInterval) { |
||||
clearInterval(instance.timeAgoInterval); |
||||
} |
||||
|
||||
instance.lastMessageTs.set(timeAgo(ts)); |
||||
|
||||
instance.timeAgoInterval = setInterval(() => { |
||||
requestAnimationFrame(() => instance.lastMessageTs.set(timeAgo(ts))); |
||||
}, 60000); |
||||
} |
||||
|
||||
Template.sidebarItem.onCreated(function() { |
||||
this.user = Users.findOne(Meteor.userId(), { fields: { username: 1 } }); |
||||
|
||||
this.lastMessageTs = new ReactiveVar(); |
||||
|
||||
this.autorun(() => { |
||||
const currentData = Template.currentData(); |
||||
|
||||
if (!currentData.lastMessage || getUserPreference(Meteor.userId(), 'sidebarViewMode') !== 'extended') { |
||||
return clearInterval(this.timeAgoInterval); |
||||
} |
||||
|
||||
if (!currentData.lastMessage._id) { |
||||
this.renderedMessage = currentData.lastMessage.msg; |
||||
return; |
||||
} |
||||
|
||||
setLastMessageTs(this, currentData.lm || currentData.lastMessage.ts); |
||||
|
||||
if (currentData.lastMessage.t === 'e2e' && currentData.lastMessage.e2e !== 'done') { |
||||
this.renderedMessage = '******'; |
||||
return; |
||||
} |
||||
|
||||
const otherUser = settings.get('UI_Use_Real_Name') ? currentData.lastMessage.u.name || currentData.lastMessage.u.username : currentData.lastMessage.u.username; |
||||
const renderedMessage = renderMessageBody(currentData.lastMessage).replace(/<br\s?\\?>/g, ' '); |
||||
const sender = this.user && this.user._id === currentData.lastMessage.u._id ? t('You') : otherUser; |
||||
|
||||
if (!currentData.isGroupChat && Meteor.userId() !== currentData.lastMessage.u._id) { |
||||
this.renderedMessage = currentData.lastMessage.msg === '' ? t('Sent_an_attachment') : renderedMessage; |
||||
} else { |
||||
this.renderedMessage = currentData.lastMessage.msg === '' ? t('user_sent_an_attachment', { user: sender }) : `${ sender }: ${ renderedMessage }`; |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
Template.sidebarItem.events({ |
||||
'click [data-id], click .sidebar-item__link'() { |
||||
return menu.close(); |
||||
}, |
||||
'click .sidebar-item__menu'(e) { |
||||
e.stopPropagation(); // to not close the menu
|
||||
e.preventDefault(); |
||||
|
||||
const canLeave = () => { |
||||
const roomData = Session.get(`roomData${ this.rid }`); |
||||
|
||||
if (!roomData) { return false; } |
||||
|
||||
if (roomData.t === 'c' && !hasAtLeastOnePermission('leave-c')) { return false; } |
||||
if (roomData.t === 'p' && !hasAtLeastOnePermission('leave-p')) { return false; } |
||||
|
||||
return !(((roomData.cl != null) && !roomData.cl) || ['d', 'l'].includes(roomData.t)); |
||||
}; |
||||
|
||||
const canFavorite = settings.get('Favorite_Rooms') && ChatSubscription.find({ rid: this.rid }).count() > 0; |
||||
const isFavorite = () => { |
||||
const sub = ChatSubscription.findOne({ rid: this.rid }, { fields: { f: 1 } }); |
||||
if (((sub != null ? sub.f : undefined) != null) && sub.f) { |
||||
return true; |
||||
} |
||||
return false; |
||||
}; |
||||
|
||||
const items = [{ |
||||
icon: 'eye-off', |
||||
name: t('Hide_room'), |
||||
type: 'sidebar-item', |
||||
id: 'hide', |
||||
}]; |
||||
|
||||
if (this.alert) { |
||||
items.push({ |
||||
icon: 'flag', |
||||
name: t('Mark_read'), |
||||
type: 'sidebar-item', |
||||
id: 'read', |
||||
}); |
||||
} else { |
||||
items.push({ |
||||
icon: 'flag', |
||||
name: t('Mark_unread'), |
||||
type: 'sidebar-item', |
||||
id: 'unread', |
||||
}); |
||||
} |
||||
|
||||
if (canFavorite) { |
||||
items.push({ |
||||
icon: 'star', |
||||
name: t(isFavorite() ? 'Unfavorite' : 'Favorite'), |
||||
modifier: isFavorite() ? 'star-filled' : 'star', |
||||
type: 'sidebar-item', |
||||
id: 'favorite', |
||||
}); |
||||
} |
||||
|
||||
if (canLeave()) { |
||||
items.push({ |
||||
icon: 'sign-out', |
||||
name: t('Leave_room'), |
||||
type: 'sidebar-item', |
||||
id: 'leave', |
||||
modifier: 'error', |
||||
}); |
||||
} |
||||
|
||||
const config = { |
||||
popoverClass: 'sidebar-item', |
||||
columns: [ |
||||
{ |
||||
groups: [ |
||||
{ |
||||
items, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
data: { |
||||
template: this.t, |
||||
rid: this.rid, |
||||
name: this.name, |
||||
}, |
||||
currentTarget: e.currentTarget, |
||||
offsetHorizontal: -e.currentTarget.clientWidth, |
||||
}; |
||||
|
||||
popover.open(config); |
||||
}, |
||||
}); |
||||
|
||||
Template.sidebarItemIcon.helpers({ |
||||
uid() { |
||||
if (!this.rid) { |
||||
return this._id; |
||||
} |
||||
return getUidDirectMessage(this.rid); |
||||
}, |
||||
isRoom() { |
||||
return this.rid || this._id; |
||||
}, |
||||
status() { |
||||
if (this.t === 'd') { |
||||
return Session.get(`user_${ this.username }_status`) || 'offline'; |
||||
} |
||||
|
||||
if (this.t === 'l') { |
||||
return roomTypes.getUserStatus('l', this.rid) || 'offline'; |
||||
} |
||||
|
||||
return false; |
||||
}, |
||||
}); |
@ -1,30 +0,0 @@ |
||||
<template name="toolbar"> |
||||
<div class="toolbar"> |
||||
<form class="toolbar__wrapper" role="search"> |
||||
<div class="toolbar__search"> |
||||
<div class="rc-input"> |
||||
<label class="rc-input__label"> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{> icon block="rc-input__icon-svg" icon="magnifier"}} |
||||
</div> |
||||
<input type="text" class="rc-input__element rc-input__element--small js-search" placeholder="{{getPlaceholder}}"> |
||||
<div class="rc-input__icon rc-input__icon--right"> |
||||
{{> icon block="rc-input__icon-svg" icon="plus"}} |
||||
</div> |
||||
</div> |
||||
</label> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
{{> messagePopup popupConfig}} |
||||
</template> |
||||
|
||||
<template name="toolbarSearchList"> |
||||
{{> chatRoomItem . icon=icon}} |
||||
</template> |
||||
|
||||
<template name="toolbarSearchListEmpty"> |
||||
{{_ "No_results_found"}} |
||||
</template> |
@ -1,203 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Session } from 'meteor/session'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { toolbarSearch } from './sidebarHeader'; |
||||
import { Rooms, Subscriptions } from '../../models'; |
||||
import { roomTypes } from '../../utils'; |
||||
import { hasAtLeastOnePermission } from '../../authorization'; |
||||
import { menu } from '../../ui-utils'; |
||||
import { escapeRegExp } from '../../../client/lib/escapeRegExp'; |
||||
|
||||
let filterText = ''; |
||||
let usernamesFromClient; |
||||
let resultsFromClient; |
||||
|
||||
const isLoading = new ReactiveVar(false); |
||||
|
||||
const getFromServer = (cb, type) => { |
||||
isLoading.set(true); |
||||
const currentFilter = filterText; |
||||
|
||||
Meteor.call('spotlight', currentFilter, usernamesFromClient, type, (err, results) => { |
||||
if (currentFilter !== filterText) { |
||||
return; |
||||
} |
||||
|
||||
isLoading.set(false); |
||||
|
||||
if (err) { |
||||
console.log(err); |
||||
return false; |
||||
} |
||||
|
||||
let exactUser = null; |
||||
let exactRoom = null; |
||||
if (results.users[0] && results.users[0].username === currentFilter) { |
||||
exactUser = results.users.shift(); |
||||
} |
||||
if (results.rooms[0] && results.rooms[0].username === currentFilter) { |
||||
exactRoom = results.rooms.shift(); |
||||
} |
||||
|
||||
const resultsFromServer = []; |
||||
|
||||
const roomFilter = (room) => !resultsFromClient.find((item) => [item.rid, item._id].includes(room._id)); |
||||
const userMap = (user) => ({ |
||||
_id: user._id, |
||||
t: 'd', |
||||
name: user.username, |
||||
fname: user.name, |
||||
avatarETag: user.avatarETag, |
||||
}); |
||||
|
||||
resultsFromServer.push(...results.users.map(userMap)); |
||||
resultsFromServer.push(...results.rooms.filter(roomFilter)); |
||||
|
||||
if (resultsFromServer.length || exactUser || exactRoom) { |
||||
exactRoom = exactRoom ? [roomFilter(exactRoom)] : []; |
||||
exactUser = exactUser ? [userMap(exactUser)] : []; |
||||
const combinedResults = exactUser.concat(exactRoom, resultsFromClient, resultsFromServer); |
||||
cb(combinedResults); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
const getFromServerDebounced = _.debounce(getFromServer, 500); |
||||
|
||||
Template.toolbar.helpers({ |
||||
results() { |
||||
return Template.instance().resultsList.get(); |
||||
}, |
||||
getPlaceholder() { |
||||
let placeholder = TAPi18n.__('Search'); |
||||
|
||||
if (!Meteor.Device.isDesktop()) { |
||||
return placeholder; |
||||
} if (window.navigator.platform.toLowerCase().includes('mac')) { |
||||
placeholder = `${ placeholder } (\u2318+K)`; |
||||
} else { |
||||
placeholder = `${ placeholder } (\u2303+K)`; |
||||
} |
||||
|
||||
return placeholder; |
||||
}, |
||||
popupConfig() { |
||||
const config = { |
||||
collection: Meteor.userId() ? Subscriptions : Rooms, |
||||
template: 'toolbarSearchList', |
||||
sidebar: true, |
||||
emptyTemplate: 'toolbarSearchListEmpty', |
||||
input: '.toolbar__search .rc-input__element', |
||||
cleanOnEnter: true, |
||||
closeOnEsc: true, |
||||
blurOnSelectItem: true, |
||||
isLoading, |
||||
open: Template.instance().open, |
||||
getFilter(collection, filter, cb) { |
||||
filterText = filter; |
||||
|
||||
const type = { |
||||
users: true, |
||||
rooms: true, |
||||
}; |
||||
|
||||
const query = { |
||||
rid: { |
||||
$ne: Session.get('openedRoom'), |
||||
}, |
||||
}; |
||||
|
||||
if (!Meteor.userId()) { |
||||
query._id = query.rid; |
||||
delete query.rid; |
||||
} |
||||
const searchForChannels = filterText[0] === '#'; |
||||
const searchForDMs = filterText[0] === '@'; |
||||
if (searchForChannels) { |
||||
filterText = filterText.slice(1); |
||||
type.users = false; |
||||
query.t = 'c'; |
||||
} |
||||
|
||||
if (searchForDMs) { |
||||
filterText = filterText.slice(1); |
||||
type.rooms = false; |
||||
query.t = 'd'; |
||||
} |
||||
|
||||
const searchQuery = new RegExp(escapeRegExp(filterText), 'i'); |
||||
query.$or = [ |
||||
{ name: searchQuery }, |
||||
{ fname: searchQuery }, |
||||
]; |
||||
|
||||
resultsFromClient = collection.find(query, { limit: 20, sort: { unread: -1, ls: -1 } }).fetch(); |
||||
|
||||
const resultsFromClientLength = resultsFromClient.length; |
||||
const user = Meteor.users.findOne(Meteor.userId(), { fields: { name: 1, username: 1 } }); |
||||
if (user) { |
||||
usernamesFromClient = [user]; |
||||
} |
||||
|
||||
for (let i = 0; i < resultsFromClientLength; i++) { |
||||
if (resultsFromClient[i].t === 'd') { |
||||
usernamesFromClient.push(resultsFromClient[i].name); |
||||
} |
||||
} |
||||
|
||||
cb(resultsFromClient); |
||||
|
||||
// Use `filter` here to get results for `#` or `@` filter only
|
||||
if (resultsFromClient.length < 20) { |
||||
getFromServerDebounced(cb, type); |
||||
} |
||||
}, |
||||
|
||||
getValue(_id, collection, records) { |
||||
const doc = _.findWhere(records, { _id }); |
||||
|
||||
roomTypes.openRouteLink(doc.t, doc, FlowRouter.current().queryParams); |
||||
menu.close(); |
||||
}, |
||||
}; |
||||
|
||||
return config; |
||||
}, |
||||
}); |
||||
|
||||
Template.toolbar.events({ |
||||
'submit form'(e) { |
||||
e.preventDefault(); |
||||
return false; |
||||
}, |
||||
|
||||
'click [role="search"] input'() { |
||||
toolbarSearch.shortcut = false; |
||||
}, |
||||
|
||||
'click [role="search"] button, touchend [role="search"] button'(e) { |
||||
if (hasAtLeastOnePermission(['create-c', 'create-p'])) { |
||||
// TODO: resolve this name menu/sidebar/sidebav/flex...
|
||||
menu.close(); |
||||
FlowRouter.go('create-channel'); |
||||
} else { |
||||
e.preventDefault(); |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
Template.toolbar.onRendered(function() { |
||||
this.$('.js-search').select().focus(); |
||||
}); |
||||
|
||||
Template.toolbar.onCreated(function() { |
||||
this.open = new ReactiveVar(true); |
||||
|
||||
Tracker.autorun(() => !this.open.get() && toolbarSearch.close()); |
||||
}); |
@ -1,8 +0,0 @@ |
||||
<template name="pageSettingsContainer"> |
||||
<section class="page-container page-home page-static page-settings content-background-color"> |
||||
{{> header sectionName=pageTitle}} |
||||
<div class="content {{#if noScroll}}no-scroll{{/if}}"> |
||||
{{> Template.dynamic template=pageTemplate}} |
||||
</div> |
||||
</section> |
||||
</template> |
@ -1,4 +0,0 @@ |
||||
import React from 'react'; |
||||
import { Button, Icon } from '@rocket.chat/fuselage'; |
||||
// TODO fuselage
|
||||
export const ActionButton = ({ icon, ...props }) => <Button {...props} square ghost small flexShrink={0}><Icon name={icon} size='x20'/></Button>; |
@ -1,23 +1,55 @@ |
||||
import React from 'react'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React, { useEffect, useState } from 'react'; |
||||
import { StatusBullet } from '@rocket.chat/fuselage'; |
||||
|
||||
const Base = (props) => <Box size='x12' borderRadius='full' flexShrink={0} {...props}/>; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { Presence } from '../../lib/presence'; |
||||
|
||||
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) { |
||||
export const UserStatus = React.memo(({ small, ...props }) => { |
||||
const size = small ? 'small' : 'large'; |
||||
const t = useTranslation(); |
||||
switch (props.status) { |
||||
case 'online': |
||||
return <Online/>; |
||||
return <StatusBullet size={size} title={t('Online')} {...props}/>; |
||||
case 'busy': |
||||
return <Busy/>; |
||||
return <StatusBullet size={size} title={t('Busy')} {...props}/>; |
||||
case 'away': |
||||
return <Away/>; |
||||
return <StatusBullet size={size} title={t('Away')} {...props}/>; |
||||
case 'Offline': |
||||
return <StatusBullet size={size} title={t('Offline')} {...props}/>; |
||||
default: |
||||
return <Offline/>; |
||||
return <StatusBullet size={size} title={t('Loading')} {...props}/>; |
||||
} |
||||
}); |
||||
|
||||
export const Busy = (props) => <UserStatus status='busy' {...props}/>; |
||||
export const Away = (props) => <UserStatus status='away' {...props}/>; |
||||
export const Online = (props) => <UserStatus status='online' {...props}/>; |
||||
export const Offline = (props) => <UserStatus status='offline' {...props}/>; |
||||
export const Loading = (props) => <UserStatus {...props}/>; |
||||
|
||||
export const colors = { |
||||
busy: 'danger-500', |
||||
away: 'warning-600', |
||||
online: 'success-500', |
||||
offline: 'neutral-600', |
||||
}; |
||||
|
||||
export const usePresence = (uid, presence) => { |
||||
const [status, setStatus] = useState(presence); |
||||
useEffect(() => { |
||||
const handle = ({ status = 'offline' }) => { |
||||
setStatus(status); |
||||
}; |
||||
Presence.listen(uid, handle); |
||||
return () => { |
||||
Presence.stop(uid, handle); |
||||
}; |
||||
}, [uid]); |
||||
|
||||
return status; |
||||
}; |
||||
|
||||
export const ReactiveUserStatus = React.memo(({ uid, presence, ...props }) => { |
||||
const status = usePresence(uid, presence); |
||||
return <UserStatus status={status} {...props} />; |
||||
}); |
||||
|
@ -1,8 +0,0 @@ |
||||
import React from 'react'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
|
||||
import statusColors from '../../../lib/statusColors'; |
||||
|
||||
const UserStatus = React.memo(({ status, ...props }) => <Box size='x12' borderRadius='full' backgroundColor={statusColors[status]} {...props}/>); |
||||
|
||||
export default UserStatus; |
@ -0,0 +1,26 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
|
||||
import { OmichannelRoutingConfig, Inquiries } from '../../definition/OmichannelRoutingConfig'; |
||||
|
||||
export type OmnichannelContextValue = { |
||||
inquiries: Inquiries; |
||||
enabled: boolean; |
||||
agentAvailable: boolean; |
||||
routeConfig?: OmichannelRoutingConfig; |
||||
showOmnichannelQueueLink: boolean; |
||||
}; |
||||
|
||||
export const OmnichannelContext = createContext<OmnichannelContextValue>({ |
||||
inquiries: { enabled: false }, |
||||
enabled: false, |
||||
agentAvailable: false, |
||||
showOmnichannelQueueLink: false, |
||||
}); |
||||
|
||||
export const useOmnichannel = (): OmnichannelContextValue => useContext(OmnichannelContext); |
||||
export const useOmnichannelShowQueueLink = (): boolean => useOmnichannel().showOmnichannelQueueLink; |
||||
export const useOmnichannelRouteConfig = (): OmichannelRoutingConfig | undefined => useOmnichannel().routeConfig; |
||||
export const useOmnichannelAgentAvailable = (): boolean => useOmnichannel().agentAvailable; |
||||
export const useQueuedInquiries = (): Inquiries => useOmnichannel().inquiries; |
||||
export const useOmnichannelQueueLink = (): string => '/livechat-queue'; |
||||
export const useOmnichannelEnabled = (): boolean => useOmnichannel().enabled; |
@ -0,0 +1,19 @@ |
||||
import { useRef, useEffect } from 'react'; |
||||
|
||||
export function useOutsideClick(cb) { |
||||
const ref = useRef(); |
||||
useEffect(() => { |
||||
function handleClickOutside(event) { |
||||
if (ref.current && !ref.current.contains(event.target)) { |
||||
cb(event); |
||||
} |
||||
} |
||||
|
||||
document.addEventListener('mousedown', handleClickOutside); |
||||
return () => { |
||||
document.removeEventListener('mousedown', handleClickOutside); |
||||
}; |
||||
}, [cb]); |
||||
|
||||
return ref; |
||||
} |
@ -0,0 +1,81 @@ |
||||
import EventEmitter from 'wolfy87-eventemitter'; |
||||
|
||||
import { APIClient } from '../../app/utils/client'; |
||||
|
||||
export const Presence = new EventEmitter(); |
||||
|
||||
const Statuses = new Map(); |
||||
|
||||
const getPresence = (() => { |
||||
const uids = new Set(); |
||||
|
||||
let timer; |
||||
const fetch = () => { |
||||
timer && clearTimeout(timer); |
||||
timer = setTimeout(async () => { |
||||
const params = { |
||||
ids: [...uids], |
||||
}; |
||||
|
||||
const { |
||||
users, |
||||
} = await APIClient.v1.get('users.presence', params); |
||||
|
||||
users.forEach((user) => { |
||||
Presence.emit(user._id, user); |
||||
uids.delete(user._id); |
||||
}); |
||||
|
||||
[...uids].forEach((uid) => { |
||||
Presence.emit(uid, { uid }); |
||||
}); |
||||
|
||||
uids.clear(); |
||||
}, 50); |
||||
}; |
||||
|
||||
const get = async (uid) => { |
||||
uids.add(uid); |
||||
fetch(); |
||||
}; |
||||
|
||||
Presence.on('remove', (uid) => { |
||||
if (Presence._events[uid]?.length) { |
||||
return; |
||||
} |
||||
Statuses.delete(uid); |
||||
delete Presence._events[uid]; |
||||
}); |
||||
|
||||
Presence.on('reset', () => { |
||||
Presence.once('restart', () => Object.keys(Presence._events).filter((e) => Boolean(e) && !['reset', 'restart', 'remove'].includes(e) && typeof e === 'string').forEach(get)); |
||||
}); |
||||
|
||||
return get; |
||||
})(); |
||||
|
||||
|
||||
const update = ({ _id: uid, status }) => { |
||||
Statuses.set(uid, status); |
||||
}; |
||||
|
||||
Presence.listen = async (uid, handle) => { |
||||
Presence.on(uid, handle); |
||||
Presence.on(uid, update); |
||||
Presence.on('reset', handle); |
||||
if (Statuses.has(uid)) { |
||||
return handle({ status: Statuses.get(uid) }); |
||||
} |
||||
getPresence(uid); |
||||
}; |
||||
|
||||
Presence.stop = (uid, handle) => { |
||||
Presence.off(uid, handle); |
||||
Presence.off('reset', handle); |
||||
Presence.emit('remove', uid); |
||||
}; |
||||
|
||||
Presence.reset = () => { |
||||
Presence.emit('reset', { status: 'offline' }); |
||||
Statuses.clear(); |
||||
}; |
@ -1,8 +0,0 @@ |
||||
const statusColors = { |
||||
offline: 'neutral-500', |
||||
busy: 'danger-500', |
||||
away: 'warning-500', |
||||
online: 'success-500', |
||||
}; |
||||
|
||||
export default statusColors; |
@ -0,0 +1,120 @@ |
||||
import React, { useState, useEffect, FC, useCallback, useMemo } from 'react'; |
||||
|
||||
|
||||
import { OmichannelRoutingConfig } from '../../definition/OmichannelRoutingConfig'; |
||||
import { IOmnichannelAgent } from '../../definition/IOmnichannelAgent'; |
||||
import { Notifications } from '../../app/notifications/client'; |
||||
import { OmnichannelContext, OmnichannelContextValue } from '../contexts/OmnichannelContext'; |
||||
import { useReactiveValue } from '../hooks/useReactiveValue'; |
||||
import { useUser, useUserId } from '../contexts/UserContext'; |
||||
import { useMethodData, AsyncState } from '../contexts/ServerContext'; |
||||
import { usePermission, useRole } from '../contexts/AuthorizationContext'; |
||||
import { useSetting } from '../contexts/SettingsContext'; |
||||
import { LivechatInquiry } from '../../app/livechat/client/collections/LivechatInquiry'; |
||||
import { initializeLivechatInquiryStream } from '../../app/livechat/client/lib/stream/queueManager'; |
||||
|
||||
const args = [] as any; |
||||
|
||||
const emptyContext = { |
||||
inquiries: { enabled: false }, |
||||
enabled: false, |
||||
agentAvailable: false, |
||||
showOmnichannelQueueLink: false, |
||||
} as OmnichannelContextValue; |
||||
|
||||
|
||||
const useOmnichannelInquiries = (): Array<any> => { |
||||
const uid = useUserId(); |
||||
const isOmnichannelManger = useRole('livechat-manager'); |
||||
const omnichannelPoolMaxIncoming = useSetting('Livechat_guest_pool_max_number_incoming_livechats_displayed') as number; |
||||
useEffect(() => { |
||||
const handler = async (): Promise<void> => { |
||||
initializeLivechatInquiryStream(uid, isOmnichannelManger); |
||||
}; |
||||
|
||||
(async (): Promise<void> => { |
||||
initializeLivechatInquiryStream(uid, isOmnichannelManger); |
||||
Notifications.onUser('departmentAgentData', handler); |
||||
})(); |
||||
|
||||
return (): void => { |
||||
Notifications.unUser('departmentAgentData', handler); |
||||
}; |
||||
}, [isOmnichannelManger, uid]); |
||||
|
||||
return useReactiveValue(useCallback(() => LivechatInquiry.find({ |
||||
status: 'queued', |
||||
}, { |
||||
sort: { |
||||
queueOrder: 1, |
||||
estimatedWaitingTimeQueue: 1, |
||||
estimatedServiceTimeAt: 1, |
||||
}, |
||||
limit: omnichannelPoolMaxIncoming, |
||||
}).fetch(), [omnichannelPoolMaxIncoming])); |
||||
}; |
||||
|
||||
const OmnichannelDisabledProvider: FC = ({ children }) => <OmnichannelContext.Provider value={emptyContext} children={children}/>; |
||||
|
||||
const OmnichannelManualSelectionProvider: FC<{ value: OmnichannelContextValue }> = ({ value, children }) => { |
||||
const queue = useOmnichannelInquiries(); |
||||
const showOmnichannelQueueLink = useSetting('Livechat_show_queue_list_link') as boolean && value.agentAvailable; |
||||
|
||||
const contextValue = useMemo(() => ({ |
||||
...value, |
||||
inquiries: { |
||||
enabled: true, |
||||
queue, |
||||
}, |
||||
showOmnichannelQueueLink, |
||||
}), [value, queue, showOmnichannelQueueLink]); |
||||
|
||||
return <OmnichannelContext.Provider value={contextValue} children={children}/>; |
||||
}; |
||||
|
||||
const OmnichannelEnabledProvider: FC = ({ children }) => { |
||||
const omnichannelRouting = useSetting('Livechat_Routing_Method'); |
||||
const [contextValue, setContextValue] = useState<OmnichannelContextValue>({ |
||||
...emptyContext, |
||||
enabled: true, |
||||
}); |
||||
|
||||
const user = useUser() as IOmnichannelAgent; |
||||
const [routeConfig, status, reload] = useMethodData<OmichannelRoutingConfig>('livechat:getRoutingConfig', args); |
||||
|
||||
const canViewOmnichannelQueue = usePermission('view-livechat-queue'); |
||||
|
||||
useEffect(() => { |
||||
status !== AsyncState.LOADING && reload(); |
||||
}, [omnichannelRouting, reload]); // eslint-disable-line
|
||||
|
||||
useEffect(() => { |
||||
setContextValue((context) => ({ |
||||
...context, |
||||
agentAvailable: user?.statusLivechat === 'available', |
||||
})); |
||||
}, [user?.statusLivechat]); |
||||
|
||||
if (!routeConfig || !user) { |
||||
return <OmnichannelDisabledProvider children={children}/>; |
||||
} |
||||
|
||||
if (canViewOmnichannelQueue && routeConfig.showQueue && !routeConfig.autoAssignAgent && contextValue.agentAvailable) { |
||||
return <OmnichannelManualSelectionProvider value={contextValue} children={children} />; |
||||
} |
||||
|
||||
|
||||
return <OmnichannelContext.Provider value={contextValue} children={children}/>; |
||||
}; |
||||
|
||||
const OmniChannelProvider: FC = React.memo(({ children }) => { |
||||
const omniChannelEnabled = useSetting('Livechat_enabled') as boolean; |
||||
const hasAccess = usePermission('view-l-room') as boolean; |
||||
|
||||
if (!omniChannelEnabled || !hasAccess) { |
||||
return <OmnichannelDisabledProvider children={children}/>; |
||||
} |
||||
return <OmnichannelEnabledProvider children={children}/>; |
||||
}); |
||||
|
||||
export default OmniChannelProvider; |
@ -0,0 +1,39 @@ |
||||
import React, { useState } from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
const Condensed = React.memo(({ |
||||
icon, |
||||
title = '', |
||||
avatar, |
||||
actions, |
||||
href, |
||||
menuOptions, |
||||
unread, |
||||
menu, |
||||
badges, |
||||
threadUnread, |
||||
...props |
||||
}) => { |
||||
const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION); |
||||
|
||||
const handleMenu = useMutableCallback((e) => { |
||||
setMenuVisibility(e.target.offsetWidth > 0 && Boolean(menu)); |
||||
}); |
||||
return <Sidebar.Item {...props} href={href} clickable={!!href}> |
||||
{avatar && <Sidebar.Item.Avatar> |
||||
{ avatar } |
||||
</Sidebar.Item.Avatar>} |
||||
<Sidebar.Item.Content> |
||||
{ icon } |
||||
<Sidebar.Item.Title data-qa='sidebar-item-title' className={unread && 'rcx-sidebar-item--highlighted'}>{title}</Sidebar.Item.Title> {badges} <Sidebar.Item.Menu onTransitionEnd={handleMenu}>{menuVisibility && menu()}</Sidebar.Item.Menu> |
||||
</Sidebar.Item.Content> |
||||
{ actions && <Sidebar.Item.Container> |
||||
{<Sidebar.Item.Actions> |
||||
{ actions } |
||||
</Sidebar.Item.Actions>} |
||||
</Sidebar.Item.Container>} |
||||
</Sidebar.Item>; |
||||
}); |
||||
|
||||
export default Condensed; |
@ -0,0 +1,74 @@ |
||||
import React from 'react'; |
||||
import { Box, ActionButton } from '@rocket.chat/fuselage'; |
||||
|
||||
import Condensed from './Condensed'; |
||||
import * as Status from '../../components/basic/UserStatus'; |
||||
import UserAvatar from '../../components/basic/avatar/UserAvatar'; |
||||
|
||||
|
||||
export default { |
||||
title: 'Sidebar/condensed', |
||||
component: Condensed, |
||||
}; |
||||
|
||||
const actions = <> |
||||
<ActionButton primary success icon='phone'/> |
||||
<ActionButton primary danger icon='circle-cross'/> |
||||
<ActionButton primary icon='trash'/> |
||||
<ActionButton icon='phone'/> |
||||
</>; |
||||
|
||||
const avatar = <UserAvatar size='x16' url='https://via.placeholder.com/16' />; |
||||
|
||||
export const Normal = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Condensed |
||||
clickable |
||||
title='John Doe' |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
/> |
||||
</Box>; |
||||
|
||||
export const Selected = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Condensed |
||||
clickable |
||||
selected |
||||
title='John Doe' |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
/> |
||||
</Box>; |
||||
|
||||
export const Menu = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Condensed |
||||
clickable |
||||
title='John Doe' |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
menuOptions={{ |
||||
hide: { |
||||
label: { label: 'Hide', icon: 'eye-off' }, |
||||
action: () => {}, |
||||
}, |
||||
read: { |
||||
label: { label: 'Mark_read', icon: 'flag' }, |
||||
action: () => {}, |
||||
}, |
||||
favorite: { |
||||
label: { label: 'Favorite', icon: 'star' }, |
||||
action: () => {}, |
||||
}, |
||||
}} |
||||
/> |
||||
</Box>; |
||||
|
||||
export const Actions = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Condensed |
||||
clickable |
||||
selected |
||||
title='John Doe' |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
actions={actions} |
||||
/> |
||||
</Box>; |
@ -0,0 +1,58 @@ |
||||
import React, { useState } from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useShortTimeAgo } from '../../hooks/useTimeAgo'; |
||||
|
||||
const Extended = React.memo(({ |
||||
icon, |
||||
title = '', |
||||
avatar, |
||||
actions, |
||||
href, |
||||
time, |
||||
menu, |
||||
menuOptions, |
||||
subtitle = '', |
||||
badges, |
||||
threadUnread, |
||||
unread, |
||||
selected, |
||||
...props |
||||
}) => { |
||||
const formatDate = useShortTimeAgo(); |
||||
const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION); |
||||
|
||||
const handleMenu = useMutableCallback((e) => { |
||||
setMenuVisibility(e.target.offsetWidth > 0 && Boolean(menu)); |
||||
}); |
||||
|
||||
return <Sidebar.Item aria-selected={selected} selected={selected} highlighted={unread} {...props} href={href} clickable={!!href}> |
||||
{ avatar && <Sidebar.Item.Avatar> |
||||
{ avatar } |
||||
</Sidebar.Item.Avatar>} |
||||
<Sidebar.Item.Content> |
||||
<Sidebar.Item.Wrapper> |
||||
{ icon } |
||||
<Sidebar.Item.Title data-qa='sidebar-item-title' className={unread && 'rcx-sidebar-item--highlighted'}> |
||||
{ title } |
||||
</Sidebar.Item.Title> |
||||
{time && <Sidebar.Item.Time>{formatDate(time)}</Sidebar.Item.Time>} |
||||
</Sidebar.Item.Wrapper> |
||||
<Sidebar.Item.Wrapper> |
||||
<Sidebar.Item.Subtitle tabIndex='-1' className={unread && 'rcx-sidebar-item--highlighted'}> |
||||
{ subtitle } |
||||
</Sidebar.Item.Subtitle> |
||||
<Sidebar.Item.Badge>{ badges }</Sidebar.Item.Badge> |
||||
<Sidebar.Item.Menu onTransitionEnd={handleMenu}>{menuVisibility && menu()}</Sidebar.Item.Menu> |
||||
</Sidebar.Item.Wrapper> |
||||
</Sidebar.Item.Content> |
||||
{ actions && <Sidebar.Item.Container> |
||||
{<Sidebar.Item.Actions> |
||||
{ actions } |
||||
</Sidebar.Item.Actions>} |
||||
</Sidebar.Item.Container>} |
||||
</Sidebar.Item>; |
||||
}); |
||||
|
||||
export default Extended; |
@ -0,0 +1,87 @@ |
||||
import React from 'react'; |
||||
import { Box, ActionButton, Badge } from '@rocket.chat/fuselage'; |
||||
|
||||
import Extended from './Extended'; |
||||
import * as Status from '../../components/basic/UserStatus'; |
||||
import UserAvatar from '../../components/basic/avatar/UserAvatar'; |
||||
|
||||
|
||||
export default { |
||||
title: 'Sidebar/Extended', |
||||
component: Extended, |
||||
}; |
||||
|
||||
const actions = <> |
||||
<ActionButton primary success icon='phone'/> |
||||
<ActionButton primary danger icon='circle-cross'/> |
||||
<ActionButton primary icon='trash'/> |
||||
<ActionButton icon='phone'/> |
||||
</>; |
||||
|
||||
const title = <Box display='flex' flexDirection='row' w='full' alignItems='center'> |
||||
<Box flexGrow='1' withTruncatedText>John Doe</Box> |
||||
<Box fontScale='micro'>15:38</Box> |
||||
</Box>; |
||||
|
||||
const subtitle = <Box display='flex' flexDirection='row' w='full' alignItems='center'> |
||||
<Box flexGrow='1' withTruncatedText>John Doe: test 123</Box> |
||||
<Badge bg='neutral-700' color='surface' flexShrink={0}>99</Badge> |
||||
</Box>; |
||||
|
||||
const avatar = <UserAvatar size='x36' url='https://via.placeholder.com/16' />; |
||||
|
||||
export const Normal = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Extended |
||||
clickable |
||||
title={title} |
||||
subtitle={subtitle} |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
/> |
||||
</Box>; |
||||
|
||||
export const Selected = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Extended |
||||
clickable |
||||
selected |
||||
title={title} |
||||
subtitle={subtitle} |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
/> |
||||
</Box>; |
||||
|
||||
export const Menu = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Extended |
||||
clickable |
||||
title={title} |
||||
subtitle={subtitle}w |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
menuOptions={{ |
||||
hide: { |
||||
label: { label: 'Hide', icon: 'eye-off' }, |
||||
action: () => {}, |
||||
}, |
||||
read: { |
||||
label: { label: 'Mark_read', icon: 'flag' }, |
||||
action: () => {}, |
||||
}, |
||||
favorite: { |
||||
label: { label: 'Favorite', icon: 'star' }, |
||||
action: () => {}, |
||||
}, |
||||
}} |
||||
/> |
||||
</Box>; |
||||
|
||||
export const Actions = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Extended |
||||
clickable |
||||
title='John Doe' |
||||
subtitle='John Doe: test 123' |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
actions={actions} |
||||
/> |
||||
</Box>; |
@ -0,0 +1,13 @@ |
||||
import React from 'react'; |
||||
import { Box, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
import Extended from './Extended'; |
||||
|
||||
const ExtendedSkeleton = ({ showAvatar }) => <Box height='x44'><Extended |
||||
title={<Skeleton width='100%' />} |
||||
titleIcon={<Box mi='x4'>{<Skeleton width={12} />}</Box>} |
||||
subtitle={<Skeleton width='100%' />} |
||||
avatar={showAvatar && <Skeleton variant='rect' width={38} height={38}/>} |
||||
/></Box>; |
||||
|
||||
export default ExtendedSkeleton; |
@ -0,0 +1,41 @@ |
||||
import React, { useState } from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
const Medium = React.memo(({ |
||||
icon, |
||||
title = '', |
||||
avatar, |
||||
actions, |
||||
href, |
||||
menuOptions, |
||||
badges, |
||||
unread, |
||||
threadUnread, |
||||
menu, |
||||
...props |
||||
}) => { |
||||
const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION); |
||||
|
||||
const handleMenu = useMutableCallback((e) => { |
||||
setMenuVisibility(e.target.offsetWidth > 0 && Boolean(menu)); |
||||
}); |
||||
return <Sidebar.Item {...props} href={href} clickable={!!href}> |
||||
{avatar && <Sidebar.Item.Avatar> |
||||
{ avatar } |
||||
</Sidebar.Item.Avatar>} |
||||
<Sidebar.Item.Content> |
||||
{ icon } |
||||
<Sidebar.Item.Title data-qa='sidebar-item-title' className={unread && 'rcx-sidebar-item--highlighted'}>{title}</Sidebar.Item.Title> |
||||
{badges} |
||||
<Sidebar.Item.Menu onTransitionEnd={handleMenu}>{menuVisibility && menu()}</Sidebar.Item.Menu> |
||||
</Sidebar.Item.Content> |
||||
{ actions && <Sidebar.Item.Container> |
||||
{<Sidebar.Item.Actions> |
||||
{ actions } |
||||
</Sidebar.Item.Actions>} |
||||
</Sidebar.Item.Container>} |
||||
</Sidebar.Item>; |
||||
}); |
||||
|
||||
export default Medium; |
@ -0,0 +1,73 @@ |
||||
import React from 'react'; |
||||
import { Box, ActionButton } from '@rocket.chat/fuselage'; |
||||
|
||||
import Medium from './Medium'; |
||||
import * as Status from '../../components/basic/UserStatus'; |
||||
import UserAvatar from '../../components/basic/avatar/UserAvatar'; |
||||
|
||||
|
||||
export default { |
||||
title: 'Sidebar/medium', |
||||
component: Medium, |
||||
}; |
||||
|
||||
const actions = <> |
||||
<ActionButton primary success icon='phone'/> |
||||
<ActionButton primary danger icon='circle-cross'/> |
||||
<ActionButton primary icon='trash'/> |
||||
<ActionButton icon='phone'/> |
||||
</>; |
||||
|
||||
const avatar = <UserAvatar size='x28' url='https://via.placeholder.com/16' />; |
||||
|
||||
export const Normal = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Medium |
||||
clickable |
||||
title='John Doe' |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
/> |
||||
</Box>; |
||||
|
||||
export const Selected = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Medium |
||||
clickable |
||||
selected |
||||
title='John Doe' |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
/> |
||||
</Box>; |
||||
|
||||
export const Menu = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Medium |
||||
clickable |
||||
title='John Doe' |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
menuOptions={{ |
||||
hide: { |
||||
label: { label: 'Hide', icon: 'eye-off' }, |
||||
action: () => {}, |
||||
}, |
||||
read: { |
||||
label: { label: 'Mark_read', icon: 'flag' }, |
||||
action: () => {}, |
||||
}, |
||||
favorite: { |
||||
label: { label: 'Favorite', icon: 'star' }, |
||||
action: () => {}, |
||||
}, |
||||
}} |
||||
/> |
||||
</Box>; |
||||
|
||||
export const Actions = () => <Box maxWidth='x300' bg='neutral-800' borderRadius='x4'> |
||||
<Medium |
||||
clickable |
||||
title='John Doe' |
||||
titleIcon={<Box mi='x4'>{<Status.Online />}</Box>} |
||||
avatar={avatar} |
||||
actions={actions} |
||||
/> |
||||
</Box>; |
@ -0,0 +1,12 @@ |
||||
import React from 'react'; |
||||
import { Box, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
import Condensed from '../Condensed'; |
||||
|
||||
const CondensedSkeleton = ({ showAvatar }) => <Box height='x28'><Condensed |
||||
title={<Skeleton width='100%' />} |
||||
titleIcon={<Box mi='x4'>{<Skeleton width={12} />}</Box>} |
||||
avatar={showAvatar && <Skeleton variant='rect' width={16} height={16}/>} |
||||
/></Box>; |
||||
|
||||
export default CondensedSkeleton; |
@ -0,0 +1,12 @@ |
||||
import React from 'react'; |
||||
import { Box, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
import Medium from '../Medium'; |
||||
|
||||
const MediumSkeleton = ({ showAvatar }) => <Box height='x36'><Medium |
||||
title={<Skeleton width='100%' />} |
||||
titleIcon={<Box mi='x4'>{<Skeleton width={12} />}</Box>} |
||||
avatar={showAvatar && <Skeleton variant='rect' width={28} height={28}/>} |
||||
/></Box>; |
||||
|
||||
export default MediumSkeleton; |
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
|
||||
import CondensedSkeleton from './CondensedSkeleton'; |
||||
import ExtendedSkeleton from '../ExtendedSkeleton'; |
||||
import MediumSkeleton from './MediumSkeleton'; |
||||
|
||||
export default { |
||||
title: 'Sidebar/Skeleton', |
||||
}; |
||||
|
||||
export const CondensedWithAvatar = () => <CondensedSkeleton showAvatar={true} />; |
||||
export const CondensedWithoutAvatar = () => <CondensedSkeleton showAvatar={false} />; |
||||
|
||||
export const MediumWithAvatar = () => <MediumSkeleton showAvatar={true} />; |
||||
export const MediumWithoutAvatar = () => <MediumSkeleton showAvatar={false} />; |
||||
|
||||
export const ExtendedWithAvatar = () => <ExtendedSkeleton showAvatar={true} />; |
||||
export const ExtendedWithoutAvatar = () => <ExtendedSkeleton showAvatar={false} />; |
@ -0,0 +1,222 @@ |
||||
import s from 'underscore.string'; |
||||
import { Sidebar, Box, Badge } from '@rocket.chat/fuselage'; |
||||
import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useRef, useEffect } from 'react'; |
||||
import { VariableSizeList as List, areEqual } from 'react-window'; |
||||
import memoize from 'memoize-one'; |
||||
|
||||
import { usePreventDefault } from './hooks/usePreventDefault'; |
||||
import { filterMarkdown } from '../../app/markdown/lib/markdown'; |
||||
import { ReactiveUserStatus, colors } from '../components/basic/UserStatus'; |
||||
import { useTranslation } from '../contexts/TranslationContext'; |
||||
import { roomTypes } from '../../app/utils'; |
||||
import { useUserPreference } from '../contexts/UserContext'; |
||||
import RoomMenu from './RoomMenu'; |
||||
import { useSession } from '../contexts/SessionContext'; |
||||
import Omnichannel from './sections/Omnichannel'; |
||||
import { useTemplateByViewMode } from './hooks/useTemplateByViewMode'; |
||||
import { useShortcutOpenMenu } from './hooks/useShortcutOpenMenu'; |
||||
import { useAvatarTemplate } from './hooks/useAvatarTemplate'; |
||||
import { useRoomList } from './hooks/useRoomList'; |
||||
import { useSidebarPaletteColor } from './hooks/useSidebarPaletteColor'; |
||||
|
||||
const sections = { |
||||
Omnichannel, |
||||
}; |
||||
|
||||
const style = { |
||||
overflowY: 'scroll', |
||||
}; |
||||
|
||||
export const itemSizeMap = (sidebarViewMode) => { |
||||
switch (sidebarViewMode) { |
||||
case 'extended': |
||||
return 44; |
||||
case 'medium': |
||||
return 36; |
||||
case 'condensed': |
||||
default: |
||||
return 28; |
||||
} |
||||
}; |
||||
|
||||
const SidebarIcon = ({ room, small }) => { |
||||
switch (room.t) { |
||||
case 'p': |
||||
case 'c': |
||||
return <Sidebar.Item.Icon aria-hidden='true' name={roomTypes.getIcon(room)} />; |
||||
case 'l': |
||||
return <Sidebar.Item.Icon aria-hidden='true' name='headset' color={colors[room.v.status]}/>; |
||||
case 'd': |
||||
if (room.uids && room.uids.length > 2) { |
||||
return <Sidebar.Item.Icon aria-hidden='true' name='team'/>; |
||||
} |
||||
if (room.uids && room.uids.length > 0) { |
||||
return room.uids && room.uids.length && <Sidebar.Item.Icon><ReactiveUserStatus small={small && 'small'} uid={room.uids.filter((uid) => uid !== room.u._id)[0]} /></Sidebar.Item.Icon>; |
||||
} |
||||
return <Sidebar.Item.Icon aria-hidden='true' name={roomTypes.getIcon(room)}/>; |
||||
default: |
||||
return null; |
||||
} |
||||
}; |
||||
|
||||
export const createItemData = memoize((items, extended, t, SideBarItemTemplate, AvatarTemplate, openedRoom, sidebarViewMode) => ({ |
||||
items, |
||||
extended, |
||||
t, |
||||
SideBarItemTemplate, |
||||
AvatarTemplate, |
||||
openedRoom, |
||||
sidebarViewMode, |
||||
})); |
||||
|
||||
export const Row = React.memo(({ data, index, style }) => { |
||||
const { extended, items, t, SideBarItemTemplate, AvatarTemplate, openedRoom, sidebarViewMode } = data; |
||||
const item = items[index]; |
||||
if (typeof item === 'string') { |
||||
const Section = sections[item]; |
||||
return Section ? <Section aria-level='1' style={style}/> : <Sidebar.Section.Title aria-level='1' style={style}>{t(item)}</Sidebar.Section.Title>; |
||||
} |
||||
return <SideBarItemTemplateWithData sidebarViewMode={sidebarViewMode} style={style} selected={item.rid === openedRoom} t={t} room={item} extended={extended} SideBarItemTemplate={SideBarItemTemplate} AvatarTemplate={AvatarTemplate} />; |
||||
}, areEqual); |
||||
|
||||
export const normalizeSidebarMessage = ({ ...message }) => { |
||||
if (message.msg) { |
||||
return filterMarkdown(message.msg); |
||||
} |
||||
|
||||
if (message.attachments) { |
||||
const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); |
||||
|
||||
if (attachment && attachment.description) { |
||||
return s.escapeHTML(attachment.description); |
||||
} |
||||
|
||||
if (attachment && attachment.title) { |
||||
return s.escapeHTML(attachment.title); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
export default () => { |
||||
useSidebarPaletteColor(); |
||||
const listRef = useRef(); |
||||
const { ref, contentBoxSize: { blockSize = 750 } = {} } = useResizeObserver({ debounceDelay: 100 }); |
||||
|
||||
const openedRoom = useSession('openedRoom'); |
||||
|
||||
const sidebarViewMode = useUserPreference('sidebarViewMode'); |
||||
const sideBarItemTemplate = useTemplateByViewMode(); |
||||
const avatarTemplate = useAvatarTemplate(); |
||||
const extended = sidebarViewMode === 'extended'; |
||||
|
||||
const t = useTranslation(); |
||||
|
||||
const itemSize = itemSizeMap(sidebarViewMode); |
||||
const roomsList = useRoomList(); |
||||
const itemData = createItemData(roomsList, extended, t, sideBarItemTemplate, avatarTemplate, openedRoom, sidebarViewMode); |
||||
|
||||
usePreventDefault(ref); |
||||
useShortcutOpenMenu(ref); |
||||
|
||||
useEffect(() => { |
||||
listRef.current?.resetAfterIndex(0); |
||||
}, [sidebarViewMode]); |
||||
|
||||
return <Box h='full' w='full' ref={ref}> |
||||
<List |
||||
height={blockSize} |
||||
itemCount={roomsList.length} |
||||
itemSize={(index) => (typeof roomsList[index] === 'string' ? (sections[roomsList[index]] && sections[roomsList[index]].size) || 40 : itemSize)} |
||||
itemData={itemData} |
||||
overscanCount={10} |
||||
width='100%' |
||||
ref={listRef} |
||||
style={style} |
||||
> |
||||
{Row} |
||||
</List> |
||||
</Box>; |
||||
}; |
||||
|
||||
const getMessage = (room, lastMessage, t) => { |
||||
if (!lastMessage) { |
||||
return t('No_messages_yet'); |
||||
} |
||||
if (!lastMessage.u) { |
||||
return normalizeSidebarMessage(lastMessage); |
||||
} |
||||
if (lastMessage.u?.username === room.u?.username) { |
||||
return `${ t('You') }: ${ normalizeSidebarMessage(lastMessage) }`; |
||||
} |
||||
if (room.t === 'd' && room.uids.length <= 2) { |
||||
return normalizeSidebarMessage(lastMessage); |
||||
} |
||||
return `${ lastMessage.u.name || lastMessage.u.username }: ${ normalizeSidebarMessage(lastMessage) }`; |
||||
}; |
||||
|
||||
export const SideBarItemTemplateWithData = React.memo(function SideBarItemTemplateWithData({ room, id, extended, selected, SideBarItemTemplate, AvatarTemplate, t, style, sidebarViewMode }) { |
||||
const title = roomTypes.getRoomName(room.t, room); |
||||
const icon = <SidebarIcon room={room} small={sidebarViewMode !== 'medium'}/>; |
||||
const href = roomTypes.getRouteLink(room.t, room); |
||||
|
||||
const { |
||||
lastMessage, |
||||
hideUnreadStatus, |
||||
unread = 0, |
||||
alert, |
||||
userMentions, |
||||
groupMentions, |
||||
tunread = [], |
||||
tunreadUser = [], |
||||
rid, |
||||
t: type, |
||||
cl, |
||||
} = room; |
||||
|
||||
const threadUnread = tunread.length > 0; |
||||
const message = extended && getMessage(room, lastMessage, t); |
||||
|
||||
const subtitle = message ? <span className='message-body--unstyled' dangerouslySetInnerHTML={{ __html: message }}/> : null; |
||||
const variant = ((userMentions || tunreadUser.length) && 'danger') || (threadUnread && 'primary') || (groupMentions && 'warning') || 'ghost'; |
||||
const badges = unread > 0 || threadUnread ? <Badge variant={ variant } flexShrink={0}>{unread + tunread?.length}</Badge> : null; |
||||
|
||||
return <SideBarItemTemplate |
||||
is='a' |
||||
id={id} |
||||
data-qa='sidebar-item' |
||||
aria-level='2' |
||||
unread={!hideUnreadStatus && (alert || unread)} |
||||
threadUnread={threadUnread} |
||||
selected={selected} |
||||
href={href} |
||||
aria-label={title} |
||||
title={title} |
||||
time={lastMessage?.ts} |
||||
subtitle={subtitle} |
||||
icon={icon} |
||||
style={style} |
||||
badges={badges} |
||||
avatar={AvatarTemplate && <AvatarTemplate {...room}/>} |
||||
menu={() => <RoomMenu rid={rid} unread={!!unread} roomOpen={false} type={type} cl={cl} name={title} status={room.status}/>} |
||||
/>; |
||||
}, (prevProps, nextProps) => { |
||||
if (['id', 'style', 'extended', 'selected', 'SideBarItemTemplate', 'AvatarTemplate', 't', 'sidebarViewMode'].some((key) => prevProps[key] !== nextProps[key])) { |
||||
return false; |
||||
} |
||||
|
||||
if (prevProps.room === nextProps.room) { |
||||
return true; |
||||
} |
||||
|
||||
if (prevProps.room._id !== nextProps.room._id) { |
||||
return false; |
||||
} |
||||
if (prevProps.room._updatedAt?.toISOString() !== nextProps.room._updatedAt?.toISOString()) { |
||||
return false; |
||||
} |
||||
if (prevProps.room.lastMessage?._updatedAt?.toISOString() !== nextProps.room.lastMessage?._updatedAt?.toISOString()) { |
||||
return false; |
||||
} |
||||
return true; |
||||
}); |
@ -0,0 +1,159 @@ |
||||
import { Option, Menu } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import { useTranslation } from '../contexts/TranslationContext'; |
||||
import { useSetting } from '../contexts/SettingsContext'; |
||||
import { useRoute } from '../contexts/RouterContext'; |
||||
import { RoomManager } from '../../app/ui-utils/client/lib/RoomManager'; |
||||
import { useMethod } from '../contexts/ServerContext'; |
||||
import { roomTypes, UiTextContext } from '../../app/utils'; |
||||
import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; |
||||
import { useUserSubscription, useUserId } from '../contexts/UserContext'; |
||||
import { usePermission } from '../contexts/AuthorizationContext'; |
||||
import { useSetModal } from '../contexts/ModalContext'; |
||||
import WarningModal from '../admin/apps/WarningModal'; |
||||
|
||||
const fields = { |
||||
f: 1, |
||||
t: 1, |
||||
name: 1, |
||||
}; |
||||
|
||||
const RoomMenu = React.memo(({ rid, unread, roomOpen, type, cl, name = '', status }) => { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const setModal = useSetModal(); |
||||
|
||||
const isAnonymous = !useUserId(); |
||||
|
||||
const closeModal = useMutableCallback(() => setModal()); |
||||
|
||||
const router = useRoute('home'); |
||||
|
||||
const subscription = useUserSubscription(rid, fields); |
||||
const canFavorite = useSetting('Favorite_Rooms'); |
||||
const isFavorite = ((subscription != null ? subscription.f : undefined) != null) && subscription.f; |
||||
|
||||
const hideRoom = useMethod('hideRoom'); |
||||
const readMessages = useMethod('readMessages'); |
||||
const unreadMessages = useMethod('unreadMessages'); |
||||
const toggleFavorite = useMethod('toggleFavorite'); |
||||
const leaveRoom = useMethod('leaveRoom'); |
||||
|
||||
const canLeaveChannel = usePermission('leave-c'); |
||||
const canLeavePrivate = usePermission('leave-p'); |
||||
|
||||
const isQueued = status === 'queued'; |
||||
|
||||
const canLeave = (() => { |
||||
if (type === 'c' && !canLeaveChannel) { return false; } |
||||
if (type === 'p' && !canLeavePrivate) { return false; } |
||||
return !(((cl != null) && !cl) || ['d', 'l'].includes(type)); |
||||
})(); |
||||
|
||||
const handleLeave = useMutableCallback(() => { |
||||
const leave = async () => { |
||||
try { |
||||
await leaveRoom(rid); |
||||
if (roomOpen) { |
||||
router.push({}); |
||||
} |
||||
RoomManager.close(rid); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}; |
||||
|
||||
const warnText = roomTypes.getConfig(type).getUiText(UiTextContext.LEAVE_WARNING); |
||||
|
||||
|
||||
setModal(<WarningModal |
||||
text={t(warnText, name)} |
||||
confirmText={t('Leave_room')} |
||||
close={closeModal} |
||||
cancel={closeModal} |
||||
cancelText={t('Cancel')} |
||||
confirm={leave} |
||||
/>); |
||||
}); |
||||
|
||||
const handleHide = useMutableCallback(async () => { |
||||
const hide = async () => { |
||||
try { |
||||
await hideRoom(rid); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
closeModal(); |
||||
}; |
||||
|
||||
const warnText = roomTypes.getConfig(type).getUiText(UiTextContext.HIDE_WARNING); |
||||
|
||||
setModal(<WarningModal |
||||
text={t(warnText, name)} |
||||
confirmText={t('Yes_hide_it')} |
||||
close={closeModal} |
||||
cancel={closeModal} |
||||
cancelText={t('Cancel')} |
||||
confirm={hide} |
||||
/>); |
||||
}); |
||||
|
||||
const handleToggleRead = useMutableCallback(async () => { |
||||
try { |
||||
if (unread) { |
||||
await readMessages(rid); |
||||
return; |
||||
} |
||||
await unreadMessages(null, rid); |
||||
if (subscription == null) { |
||||
return; |
||||
} |
||||
RoomManager.close(subscription.t + subscription.name); |
||||
|
||||
router.push({}); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const handleToggleFavorite = useMutableCallback(async () => { |
||||
try { |
||||
await toggleFavorite(rid, !isFavorite); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
}); |
||||
|
||||
const menuOptions = useMemo(() => ({ |
||||
hideRoom: { |
||||
label: { label: t('Hide'), icon: 'eye-off' }, |
||||
action: handleHide, |
||||
}, |
||||
toggleRead: { |
||||
label: { label: unread ? t('Mark_read') : t('Mark_unread'), icon: 'flag' }, |
||||
action: handleToggleRead, |
||||
}, |
||||
...canFavorite && { toggleFavorite: { |
||||
label: { label: isFavorite ? t('Unfavorite') : t('Favorite'), icon: isFavorite ? 'star-filled' : 'star' }, |
||||
action: handleToggleFavorite, |
||||
} }, |
||||
...canLeave && { leaveRoom: { |
||||
label: { label: t('Leave_room'), icon: 'sign-out' }, |
||||
action: handleLeave, |
||||
} }, |
||||
}), [canFavorite, canLeave, handleHide, handleLeave, handleToggleFavorite, handleToggleRead, isFavorite, t, unread]); |
||||
|
||||
|
||||
return !isQueued && !isAnonymous ? <Menu |
||||
rcx-sidebar-item__menu |
||||
mini |
||||
aria-keyshortcuts='alt' |
||||
tabIndex={-1} |
||||
options={menuOptions} |
||||
renderItem={({ label: { label, icon }, ...props }) => <Option label={label} title={label} icon={icon} {...props}/>} |
||||
/> : null; |
||||
}); |
||||
|
||||
export default RoomMenu; |
@ -0,0 +1,161 @@ |
||||
import React from 'react'; |
||||
|
||||
import Header from './header'; |
||||
import RoomList from './RoomList'; |
||||
// import Extended from './Item/Extended';
|
||||
// import RoomAvatar from '../basic/avatar/RoomAvatar';
|
||||
import { UserContext } from '../contexts/UserContext'; |
||||
import { SettingsContext } from '../contexts/SettingsContext'; |
||||
|
||||
export default { |
||||
title: 'Sidebar', |
||||
component: '', |
||||
}; |
||||
|
||||
// const viewModes = ['extended', 'medium', 'condensed'];
|
||||
// const sortBy = ['activity', 'alphabetical'];
|
||||
|
||||
/* |
||||
[] extended |
||||
[] com avatar |
||||
[] sem avatar |
||||
[] unread |
||||
[] sem badge |
||||
[] badges |
||||
[] normal |
||||
[] mention grupo |
||||
[] mention direta |
||||
[] last message |
||||
[] `You:` |
||||
[] No messages yet |
||||
[] Fulano: |
||||
[] yesterday |
||||
[] day month |
||||
[] medium |
||||
[] sem avatar |
||||
[] com avatar |
||||
[] sem avatar |
||||
[] unread |
||||
[] sem badge |
||||
[] badges |
||||
[] normal |
||||
[] mention grupo |
||||
[] mention direta |
||||
[] condensed |
||||
[] sem avatar |
||||
[] com avatar |
||||
[] sem avatar |
||||
[] unread |
||||
[] sem badge |
||||
[] badges |
||||
[] normal |
||||
[] mention grupo |
||||
[] mention direta |
||||
|
||||
*/ |
||||
|
||||
const subscriptions = [ |
||||
{ |
||||
_id: '3Bysd8GrmkWBdS9RT', |
||||
open: true, |
||||
alert: true, |
||||
unread: 0, |
||||
userMentions: 0, |
||||
groupMentions: 0, |
||||
ts: '2020-10-01T17:01:51.476Z', |
||||
rid: 'GENERAL', |
||||
name: 'general', |
||||
t: 'c', |
||||
type: 'c', |
||||
u: { |
||||
_id: '5yLFEABCSoqR5vozz', |
||||
username: 'yyy', |
||||
name: 'yyy', |
||||
}, |
||||
_updatedAt: '2020-10-19T16:04:45.472Z', |
||||
ls: '2020-10-19T16:02:26.649Z', |
||||
lr: '2020-10-01T17:38:00.321Z', |
||||
tunread: [], |
||||
usernames: [], |
||||
lastMessage: { |
||||
_id: '5ZpfZg5R25aRZjDWp', |
||||
rid: 'GENERAL', |
||||
msg: 'teste', |
||||
ts: '2020-10-19T16:04:45.427Z', |
||||
u: { |
||||
_id: 'fmdXpuxjFivuqfAPu', |
||||
username: 'gabriellsh', |
||||
name: 'Gabriel Henriques', |
||||
}, |
||||
_updatedAt: '2020-10-19T16:04:45.454Z', |
||||
mentions: [], |
||||
channels: [], |
||||
}, |
||||
lm: '2020-10-19T16:04:45.427Z', |
||||
lowerCaseName: 'general', |
||||
lowerCaseFName: 'general', |
||||
}, |
||||
]; |
||||
|
||||
// const t = (text) => text;
|
||||
|
||||
const userPreferences = { |
||||
sidebarViewMode: 'medium', |
||||
sidebarHideAvatar: false, |
||||
sidebarGroupByType: true, |
||||
sidebarShowFavorites: true, |
||||
sidebarShowDiscussion: true, |
||||
sidebarShowUnread: true, |
||||
sidebarSortby: 'activity', |
||||
}; |
||||
|
||||
const settings = { |
||||
UI_Use_Real_Name: true, |
||||
}; |
||||
|
||||
const userId = 123; |
||||
const userContextValue = { |
||||
userId, |
||||
user: { _id: userId }, |
||||
queryPreference: (pref) => ({ |
||||
getCurrentValue: () => userPreferences[pref], |
||||
subscribe: () => () => undefined, |
||||
}), |
||||
querySubscriptions: () => ({ |
||||
getCurrentValue: () => subscriptions, |
||||
subscribe: () => () => undefined, |
||||
}), |
||||
querySubscription: () => ({ |
||||
getCurrentValue: () => undefined, |
||||
subscribe: () => () => undefined, |
||||
}), |
||||
}; |
||||
|
||||
const settingContextValue = { |
||||
hasPrivateAccess: true, |
||||
isLoading: false, |
||||
querySetting: (setting) => ({ |
||||
getCurrentValue: () => settings[setting], |
||||
subscribe: () => () => undefined, |
||||
}), |
||||
querySettings: () => ({ |
||||
getCurrentValue: () => [], |
||||
subscribe: () => () => undefined, |
||||
}), |
||||
dispatch: async () => undefined, |
||||
}; |
||||
|
||||
const Sidebar = () => <> |
||||
<SettingsContext.Provider value={settingContextValue} > |
||||
<UserContext.Provider value={userContextValue}> |
||||
<aside class='sidebar sidebar--main' role='navigation'> |
||||
<Header /> |
||||
<div class='rooms-list sidebar--custom-colors' aria-label='Channels' role='region'> |
||||
<RoomList /> |
||||
</div> |
||||
</aside> |
||||
</UserContext.Provider> |
||||
</SettingsContext.Provider> |
||||
</>; |
||||
|
||||
export const Default = () => <Sidebar />; |
@ -0,0 +1,146 @@ |
||||
import React from 'react'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { css } from '@rocket.chat/css-in-js'; |
||||
|
||||
import { popover, modal, AccountBox } from '../../../app/ui-utils'; |
||||
import { useSetting } from '../../contexts/SettingsContext'; |
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { UserStatus } from '../../components/basic/UserStatus'; |
||||
import { userStatus } from '../../../app/user-status'; |
||||
import { callbacks } from '../../../app/callbacks'; |
||||
import UserAvatar from '../../components/basic/avatar/UserAvatar'; |
||||
|
||||
const setStatus = (status, statusText) => { |
||||
AccountBox.setStatus(status, statusText); |
||||
callbacks.run('userStatusManuallySet', status); |
||||
popover.close(); |
||||
}; |
||||
|
||||
const onClick = (e, t, allowAnonymousRead) => { |
||||
if (!(Meteor.userId() == null && allowAnonymousRead)) { |
||||
const user = Meteor.user(); |
||||
const STATUS_MAP = [ |
||||
'offline', |
||||
'online', |
||||
'away', |
||||
'busy', |
||||
]; |
||||
const userStatusList = Object.keys(userStatus.list).map((key) => { |
||||
const status = userStatus.list[key]; |
||||
const name = status.localizeName ? t(status.name) : status.name; |
||||
const modifier = status.statusType || user.status; |
||||
const defaultStatus = STATUS_MAP.includes(status.id); |
||||
const statusText = defaultStatus ? null : name; |
||||
|
||||
return { |
||||
icon: 'circle', |
||||
name, |
||||
modifier, |
||||
action: () => setStatus(status.statusType, statusText), |
||||
}; |
||||
}); |
||||
|
||||
const statusText = user.statusText || t(user.status); |
||||
|
||||
userStatusList.push({ |
||||
icon: 'edit', |
||||
name: t('Edit_Status'), |
||||
type: 'open', |
||||
action: (e) => { |
||||
e.preventDefault(); |
||||
modal.open({ |
||||
title: t('Edit_Status'), |
||||
content: 'editStatus', |
||||
data: { |
||||
onSave() { |
||||
modal.close(); |
||||
}, |
||||
}, |
||||
modalClass: 'modal', |
||||
showConfirmButton: false, |
||||
showCancelButton: false, |
||||
confirmOnEnter: false, |
||||
}); |
||||
}, |
||||
}); |
||||
|
||||
const config = { |
||||
popoverClass: 'sidebar-header', |
||||
columns: [ |
||||
{ |
||||
groups: [ |
||||
{ |
||||
title: user.name, |
||||
items: [{ |
||||
icon: 'circle', |
||||
name: statusText, |
||||
modifier: user.status, |
||||
}], |
||||
}, |
||||
{ |
||||
title: t('User'), |
||||
items: userStatusList, |
||||
}, |
||||
{ |
||||
items: [ |
||||
{ |
||||
icon: 'user', |
||||
name: t('My_Account'), |
||||
type: 'open', |
||||
id: 'account', |
||||
action: () => { |
||||
FlowRouter.go('account'); |
||||
popover.close(); |
||||
}, |
||||
}, |
||||
{ |
||||
icon: 'sign-out', |
||||
name: t('Logout'), |
||||
type: 'open', |
||||
id: 'logout', |
||||
action: () => { |
||||
Meteor.logout(() => { |
||||
callbacks.run('afterLogoutCleanUp', user); |
||||
Meteor.call('logoutCleanUp', user); |
||||
FlowRouter.go('home'); |
||||
popover.close(); |
||||
}); |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
currentTarget: e.currentTarget, |
||||
offsetVertical: e.currentTarget.clientHeight + 10, |
||||
}; |
||||
|
||||
popover.open(config); |
||||
} |
||||
}; |
||||
|
||||
export default React.memo(({ user = {} }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const { |
||||
_id: uid, |
||||
status = !uid && 'online', |
||||
username = 'Anonymous', |
||||
avatarETag, |
||||
} = user; |
||||
|
||||
const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); |
||||
|
||||
const handleClick = useMutableCallback((e) => uid && onClick(e, t, allowAnonymousRead)); |
||||
|
||||
return <Box position='relative' onClick={handleClick} className={css`cursor: pointer;`} data-qa='sidebar-avatar-button'> |
||||
<UserAvatar size='x24' username={username} etag={avatarETag}/> |
||||
<Box className={css`bottom: 0; right: 0;`} justifyContent='center' alignItems='center'display='flex' overflow='hidden' size='x12' borderWidth='x2' position='absolute' bg='neutral-200' borderColor='neutral-200' borderRadius='full' mie='neg-x2' mbe='neg-x2'> |
||||
<UserStatus small status={status}/> |
||||
</Box> |
||||
</Box>; |
||||
}); |
@ -0,0 +1,92 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { popover, modal } from '../../../../app/ui-utils'; |
||||
import { useAtLeastOnePermission, usePermission } from '../../../contexts/AuthorizationContext'; |
||||
import { useSetting } from '../../../contexts/SettingsContext'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
|
||||
const CREATE_ROOM_PERMISSIONS = ['create-c', 'create-p', 'create-d', 'start-discussion', 'start-discussion-other-user']; |
||||
|
||||
const CREATE_CHANNEL_PERMISSIONS = ['create-c', 'create-p']; |
||||
|
||||
const CREATE_DISCUSSION_PERMISSIONS = ['start-discussion', 'start-discussion-other-user']; |
||||
|
||||
const openPopover = (e, items) => popover.open({ |
||||
columns: [ |
||||
{ |
||||
groups: [ |
||||
{ |
||||
items, |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
currentTarget: e.currentTarget, |
||||
offsetVertical: e.currentTarget.clientHeight + 10, |
||||
}); |
||||
|
||||
const useAction = (title, content) => useMutableCallback((e) => { |
||||
e.preventDefault(); |
||||
modal.open({ |
||||
title, |
||||
content, |
||||
data: { |
||||
onCreate() { |
||||
modal.close(); |
||||
}, |
||||
}, |
||||
modifier: 'modal', |
||||
showConfirmButton: false, |
||||
showCancelButton: false, |
||||
confirmOnEnter: false, |
||||
}); |
||||
}); |
||||
|
||||
const CreateRoom = (props) => { |
||||
const t = useTranslation(); |
||||
const showCreate = useAtLeastOnePermission(CREATE_ROOM_PERMISSIONS); |
||||
|
||||
const canCreateChannel = useAtLeastOnePermission(CREATE_CHANNEL_PERMISSIONS); |
||||
const canCreateDirectMessages = usePermission('create-d'); |
||||
const canCreateDiscussion = useAtLeastOnePermission(CREATE_DISCUSSION_PERMISSIONS); |
||||
|
||||
const createChannel = useAction(t('Create_A_New_Channel'), 'createChannel'); |
||||
const createDirectMessage = useAction(t('Direct_Messages'), 'CreateDirectMessage'); |
||||
const createDiscussion = useAction(t('Discussion_title'), 'CreateDiscussion'); |
||||
|
||||
const discussionEnabled = useSetting('Discussion_enabled'); |
||||
|
||||
const items = useMemo(() => [ |
||||
canCreateChannel && { |
||||
icon: 'hashtag', |
||||
name: t('Channel'), |
||||
qa: 'sidebar-create-channel', |
||||
action: createChannel, |
||||
}, |
||||
canCreateDirectMessages && { |
||||
icon: 'team', |
||||
name: t('Direct_Messages'), |
||||
qa: 'sidebar-create-dm', |
||||
action: createDirectMessage, |
||||
}, |
||||
discussionEnabled && canCreateDiscussion && { |
||||
icon: 'discussion', |
||||
name: t('Discussion'), |
||||
qa: 'sidebar-create-discussion', |
||||
action: createDiscussion, |
||||
}, |
||||
].filter(Boolean), [canCreateChannel, canCreateDirectMessages, canCreateDiscussion, createChannel, createDirectMessage, createDiscussion, discussionEnabled, t]); |
||||
|
||||
const onClick = useMutableCallback((e) => { |
||||
if (items.length === 1) { |
||||
return items[0].action(e); |
||||
} |
||||
openPopover(e, items); |
||||
}); |
||||
|
||||
return showCreate ? <Sidebar.TopBar.Action {...props} icon='edit-rounded' onClick={onClick}/> : null; |
||||
}; |
||||
|
||||
export default CreateRoom; |
@ -0,0 +1,14 @@ |
||||
import React from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useRoute } from '../../../contexts/RouterContext'; |
||||
|
||||
const Directory = (props) => { |
||||
const directoryRoute = useRoute('directory'); |
||||
const handleDirectory = useMutableCallback(() => directoryRoute.push({})); |
||||
|
||||
return <Sidebar.TopBar.Action {...props} icon='globe' onClick={handleDirectory}/>; |
||||
}; |
||||
|
||||
export default Directory; |
@ -0,0 +1,16 @@ |
||||
import React from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import { useRoute } from '../../../contexts/RouterContext'; |
||||
import { useSetting } from '../../../contexts/SettingsContext'; |
||||
|
||||
const Home = (props) => { |
||||
const homeRoute = useRoute('home'); |
||||
const showHome = useSetting('Layout_Show_Home_Button'); |
||||
const handleHome = useMutableCallback(() => homeRoute.push({})); |
||||
|
||||
return showHome ? <Sidebar.TopBar.Action {...props} icon='home' onClick={handleHome}/> : null; |
||||
}; |
||||
|
||||
export default Home; |
@ -0,0 +1,14 @@ |
||||
import React from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useSessionDispatch } from '../../../contexts/SessionContext'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
|
||||
const Login = (props) => { |
||||
const setForceLogin = useSessionDispatch('forceLogin'); |
||||
const t = useTranslation(); |
||||
|
||||
return <Sidebar.TopBar.Action {...props} primary ghost={false} icon='login' title={t('Sign_in_to_start_talking')} onClick={() => setForceLogin(true)}/>; |
||||
}; |
||||
|
||||
export default Login; |
@ -0,0 +1,80 @@ |
||||
import React from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
|
||||
import { popover, AccountBox, SideNav } from '../../../../app/ui-utils'; |
||||
import { useReactiveValue } from '../../../hooks/useReactiveValue'; |
||||
import { useAtLeastOnePermission } from '../../../contexts/AuthorizationContext'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
|
||||
const ADMIN_PERMISSIONS = ['manage-emoji', 'manage-oauth-apps', 'manage-outgoing-integrations', 'manage-incoming-integrations', 'manage-own-outgoing-integrations', 'manage-own-incoming-integrations', 'manage-selected-settings', 'manage-sounds', 'view-logs', 'view-privileged-setting', 'view-room-administration', 'view-statistics', 'view-user-administration', 'access-setting-permissions']; |
||||
|
||||
const openPopover = (e, accountBoxItems, t, adminOption) => popover.open({ |
||||
popoverClass: 'sidebar-header', |
||||
columns: [ |
||||
{ |
||||
groups: [ |
||||
{ |
||||
items: accountBoxItems.map((item) => { |
||||
let action; |
||||
|
||||
if (item.href || item.sideNav) { |
||||
action = () => { |
||||
if (item.href) { |
||||
FlowRouter.go(item.href); |
||||
popover.close(); |
||||
} |
||||
if (item.sideNav) { |
||||
SideNav.setFlex(item.sideNav); |
||||
SideNav.openFlex(); |
||||
popover.close(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
icon: item.icon, |
||||
name: t(item.name), |
||||
type: 'open', |
||||
id: item.name, |
||||
href: item.href, |
||||
sideNav: item.sideNav, |
||||
action, |
||||
}; |
||||
}).concat([adminOption]), |
||||
}, |
||||
], |
||||
}, |
||||
], |
||||
currentTarget: e.currentTarget, |
||||
offsetVertical: e.currentTarget.clientHeight + 10, |
||||
}); |
||||
|
||||
const getItems = () => AccountBox.getItems(); |
||||
|
||||
const adminOption = (showAdmin, t) => (showAdmin ? { |
||||
icon: 'customize', |
||||
name: t('Administration'), |
||||
type: 'open', |
||||
id: 'administration', |
||||
action: () => { |
||||
FlowRouter.go('admin', { group: 'info' }); |
||||
popover.close(); |
||||
}, |
||||
} : undefined); |
||||
|
||||
const Menu = (props) => { |
||||
const t = useTranslation(); |
||||
const showAdmin = useAtLeastOnePermission(ADMIN_PERMISSIONS); |
||||
|
||||
const accountBoxItems = useReactiveValue(getItems); |
||||
|
||||
const onClick = useMutableCallback((e) => openPopover(e, accountBoxItems, t, adminOption(showAdmin, t))); |
||||
|
||||
const showMenu = accountBoxItems?.length > 0; |
||||
|
||||
return showAdmin || showMenu ? <Sidebar.TopBar.Action {...props} icon='menu' onClick={onClick}/> : null; |
||||
}; |
||||
|
||||
export default Menu; |
@ -0,0 +1,48 @@ |
||||
import React, { useState, useEffect } from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import tinykeys from 'tinykeys'; |
||||
|
||||
import { useOutsideClick } from '../../../hooks/useOutsideClick'; |
||||
import SearchList from '../../search/SearchList'; |
||||
|
||||
const Search = (props) => { |
||||
const [searchOpen, setSearchOpen] = useState(false); |
||||
|
||||
// const viewRef = useRef();
|
||||
|
||||
const handleCloseSearch = useMutableCallback(() => { |
||||
setSearchOpen(false); |
||||
// viewRef.current && Blaze.remove(viewRef.current);
|
||||
}); |
||||
|
||||
const openSearch = useMutableCallback(() => { |
||||
setSearchOpen(true); |
||||
}); |
||||
|
||||
const ref = useOutsideClick(handleCloseSearch); |
||||
|
||||
|
||||
useEffect(() => { |
||||
const unsubscribe = tinykeys(window, { |
||||
'$mod+K': (event) => { |
||||
event.preventDefault(); |
||||
openSearch(); |
||||
}, |
||||
'$mod+P': (event) => { |
||||
event.preventDefault(); |
||||
openSearch(); |
||||
}, |
||||
}); |
||||
return () => { |
||||
unsubscribe(); |
||||
}; |
||||
}, [openSearch]); |
||||
|
||||
return <> |
||||
<Sidebar.TopBar.Action icon='magnifier' onClick={openSearch} {...props}/> |
||||
{searchOpen && <SearchList ref={ref} onClose={handleCloseSearch}/>} |
||||
</>; |
||||
}; |
||||
|
||||
export default Search; |
@ -0,0 +1,22 @@ |
||||
import React from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
|
||||
import { popover } from '../../../../app/ui-utils'; |
||||
import { createTemplateForComponent } from '../../../reactAdapters'; |
||||
|
||||
const SortList = createTemplateForComponent('SortList', () => import('../../../components/SortList')); |
||||
|
||||
const config = (e) => ({ |
||||
template: SortList, |
||||
currentTarget: e.currentTarget, |
||||
data: { |
||||
options: [], |
||||
}, |
||||
offsetVertical: e.currentTarget.clientHeight + 10, |
||||
}); |
||||
|
||||
const onClick = (e) => { popover.open(config(e)); }; |
||||
|
||||
const Sort = (props) => <Sidebar.TopBar.Action {...props} icon='sort' onClick={onClick}/>; |
||||
|
||||
export default Sort; |
@ -0,0 +1,36 @@ |
||||
import React from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
|
||||
import Home from './actions/Home'; |
||||
import Search from './actions/Search'; |
||||
import Directory from './actions/Directory'; |
||||
import Sort from './actions/Sort'; |
||||
import CreateRoom from './actions/CreateRoom'; |
||||
import Menu from './actions/Menu'; |
||||
import Login from './actions/Login'; |
||||
import UserAvatarButton from './UserAvatarButton'; |
||||
import { useUser } from '../../contexts/UserContext'; |
||||
import { useSidebarPaletteColor } from '../hooks/useSidebarPaletteColor'; |
||||
|
||||
const HeaderWithData = () => { |
||||
const user = useUser(); |
||||
useSidebarPaletteColor(); |
||||
return <> |
||||
<Sidebar.TopBar.Section className='sidebar--custom-colors'> |
||||
<UserAvatarButton user={user}/> |
||||
<Sidebar.TopBar.Actions> |
||||
<Home /> |
||||
<Search data-qa='sidebar-search' /> |
||||
{user && <> |
||||
<Directory /> |
||||
<Sort /> |
||||
<CreateRoom data-qa='sidebar-create' /> |
||||
<Menu /> |
||||
</>} |
||||
{!user && <Login/>} |
||||
</Sidebar.TopBar.Actions> |
||||
</Sidebar.TopBar.Section> |
||||
</>; |
||||
}; |
||||
|
||||
export default React.memo(HeaderWithData); |
@ -0,0 +1,28 @@ |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import RoomAvatar from '../../components/basic/avatar/RoomAvatar'; |
||||
import { useUserPreference } from '../../contexts/UserContext'; |
||||
|
||||
export const useAvatarTemplate = () => { |
||||
const sidebarViewMode = useUserPreference('sidebarViewMode'); |
||||
const sidebarHideAvatar = useUserPreference('sidebarHideAvatar'); |
||||
return useMemo(() => { |
||||
if (sidebarHideAvatar) { |
||||
return null; |
||||
} |
||||
|
||||
const size = (() => { |
||||
switch (sidebarViewMode) { |
||||
case 'extended': |
||||
return 'x36'; |
||||
case 'medium': |
||||
return 'x28'; |
||||
case 'condensed': |
||||
default: |
||||
return 'x16'; |
||||
} |
||||
})(); |
||||
|
||||
return (room) => <RoomAvatar size={size} room={{ ...room, _id: room.rid || room._id, type: room.t }} />; |
||||
}, [sidebarHideAvatar, sidebarViewMode]); |
||||
}; |
@ -0,0 +1,20 @@ |
||||
import { useEffect } from 'react'; |
||||
|
||||
export const usePreventDefault = (ref) => { |
||||
// Flowrouter uses an addEventListener on the document to capture any clink link, since the react synthetic event use an addEventListener on the document too,
|
||||
// it is impossible/hard to determine which one will happen before and prevent/stop propagation, so feel free to remove this effect after remove flow router :)
|
||||
|
||||
useEffect(() => { |
||||
const { current } = ref; |
||||
const stopPropagation = (e) => { |
||||
if ([e.target.nodeName, e.target.parentElement.nodeName].includes('BUTTON')) { |
||||
e.preventDefault(); |
||||
} |
||||
}; |
||||
current?.addEventListener('click', stopPropagation); |
||||
|
||||
return () => current?.addEventListener('click', stopPropagation); |
||||
}, [ref]); |
||||
|
||||
return { ref }; |
||||
}; |
@ -0,0 +1,19 @@ |
||||
import { useMemo } from 'react'; |
||||
|
||||
import { useSetting } from '../../contexts/SettingsContext'; |
||||
import { useUserPreference } from '../../contexts/UserContext'; |
||||
|
||||
export const useQueryOptions = () => { |
||||
const sortBy = useUserPreference('sidebarSortby'); |
||||
const showRealName = useSetting('UI_Use_Real_Name'); |
||||
|
||||
return useMemo(() => ({ |
||||
sort: { |
||||
...sortBy === 'activity' && { lm: -1 }, |
||||
...sortBy !== 'activity' && { |
||||
...showRealName && { lowerCaseFName: /descending/.test(sortBy) ? -1 : 1 }, |
||||
...!showRealName && { lowerCaseName: /descending/.test(sortBy) ? -1 : 1 }, |
||||
}, |
||||
}, |
||||
}), [sortBy, showRealName]); |
||||
}; |
@ -0,0 +1,80 @@ |
||||
import { useMemo } from 'react'; |
||||
|
||||
import { useQueuedInquiries, useOmnichannelEnabled } from '../../contexts/OmnichannelContext'; |
||||
import { useUserPreference, useUserSubscriptions } from '../../contexts/UserContext'; |
||||
import { useQueryOptions } from './useQueryOptions'; |
||||
import { ISubscription } from '../../../definition/ISubscription'; |
||||
|
||||
const query = { open: { $ne: false } }; |
||||
|
||||
export const useRoomList = (): Array<ISubscription> => { |
||||
const showOmnichannel = useOmnichannelEnabled(); |
||||
const sidebarGroupByType = useUserPreference('sidebarGroupByType'); |
||||
const favoritesEnabled = useUserPreference('sidebarShowFavorites'); |
||||
const showDiscussion = useUserPreference('sidebarShowDiscussion'); |
||||
const sidebarShowUnread = useUserPreference('sidebarShowUnread'); |
||||
|
||||
const options = useQueryOptions(); |
||||
|
||||
const rooms = useUserSubscriptions(query, options); |
||||
|
||||
const inquiries = useQueuedInquiries(); |
||||
|
||||
return useMemo(() => { |
||||
const favorite = new Set(); |
||||
const omnichannel = new Set(); |
||||
const unread = new Set(); |
||||
const _private = new Set(); |
||||
const _public = new Set(); |
||||
const direct = new Set(); |
||||
const discussion = new Set(); |
||||
const conversation = new Set(); |
||||
|
||||
rooms.forEach((room) => { |
||||
if (sidebarShowUnread && (room.alert || room.unread) && !room.hideUnreadStatus) { |
||||
return unread.add(room); |
||||
} |
||||
|
||||
if (favoritesEnabled && room.f) { |
||||
return favorite.add(room); |
||||
} |
||||
|
||||
if (showDiscussion && room.prid) { |
||||
return discussion.add(room); |
||||
} |
||||
|
||||
if (room.t === 'c') { |
||||
_public.add(room); |
||||
} |
||||
|
||||
if (room.t === 'p') { |
||||
_private.add(room); |
||||
} |
||||
|
||||
if (room.t === 'l') { |
||||
return showOmnichannel && omnichannel.add(room); |
||||
} |
||||
|
||||
if (room.t === 'd') { |
||||
direct.add(room); |
||||
} |
||||
|
||||
conversation.add(room); |
||||
}); |
||||
|
||||
const groups = new Map(); |
||||
showOmnichannel && inquiries.enabled && groups.set('Omnichannel', []); |
||||
showOmnichannel && !inquiries.enabled && groups.set('Omnichannel', omnichannel); |
||||
showOmnichannel && inquiries.enabled && inquiries.queue.length && groups.set('Incoming_Livechats', inquiries.queue); |
||||
showOmnichannel && inquiries.enabled && omnichannel.size && groups.set('Open_Livechats', omnichannel); |
||||
sidebarShowUnread && unread.size && groups.set('Unread', unread); |
||||
favoritesEnabled && favorite.size && groups.set('Favorites', favorite); |
||||
showDiscussion && discussion.size && groups.set('Discussions', discussion); |
||||
sidebarGroupByType && _private.size && groups.set('Private', _private); |
||||
sidebarGroupByType && _public.size && groups.set('Public', _public); |
||||
sidebarGroupByType && direct.size && groups.set('Direct', direct); |
||||
!sidebarGroupByType && groups.set('Conversations', conversation); |
||||
return [...groups.entries()].flatMap(([key, group]) => [key, ...group]); |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rooms, showOmnichannel, inquiries.enabled, inquiries.enabled && inquiries.queue, sidebarShowUnread, favoritesEnabled, showDiscussion, sidebarGroupByType]); |
||||
}; |
@ -0,0 +1,20 @@ |
||||
import { useEffect } from 'react'; |
||||
import tinykeys from 'tinykeys'; |
||||
|
||||
// used to open the menu option by keyboard
|
||||
export const useShortcutOpenMenu = (ref) => { |
||||
useEffect(() => { |
||||
const unsubscribe = tinykeys(ref.current, { |
||||
Alt: (event) => { |
||||
if (!event.target.className.includes('rcx-sidebar-item')) { |
||||
return; |
||||
} |
||||
event.preventDefault(); |
||||
event.target.querySelector('button')?.click(); |
||||
}, |
||||
}); |
||||
return () => { |
||||
unsubscribe(); |
||||
}; |
||||
}, []); |
||||
}; |
@ -0,0 +1,232 @@ |
||||
import { useLayoutEffect, useEffect, useMemo } from 'react'; |
||||
import colors from '@rocket.chat/fuselage-tokens/colors'; |
||||
|
||||
import { useSettings } from '../../contexts/SettingsContext'; |
||||
import { isIE11 } from '../../../app/ui-utils/client/lib/isIE11.js'; |
||||
|
||||
const isInternetExplorer11 = isIE11(); |
||||
|
||||
const oldPallet = { |
||||
'color-dark-100': '#0c0d0f', |
||||
'color-dark-90': '#1e232a', |
||||
'color-dark-80': '#2e343e', |
||||
'color-dark-70': '#53585f', |
||||
'color-dark-30': '#9da2a9', |
||||
'color-dark-20': '#caced1', |
||||
'color-dark-10': '#e0e5e8', |
||||
'color-dark-05': '#f1f2f4', |
||||
'color-dark-blue': '#175cc4', |
||||
'color-blue': '#1d74f5', |
||||
'color-light-blue': '#4eb2f5', |
||||
'color-lighter-blue': '#e8f2ff', |
||||
'color-purple': '#861da8', |
||||
'color-red': '#f5455c', |
||||
'color-dark-red': '#e0364d', |
||||
'color-orange': '#f38c39', |
||||
'color-yellow': '#ffd21f', |
||||
'color-dark-yellow': '#f6c502', |
||||
'color-green': '#2de0a5', |
||||
'color-dark-green': '#26d198', |
||||
'color-darkest': '#1f2329', |
||||
'color-dark': '#2f343d', |
||||
'color-dark-medium': '#414852', |
||||
'color-dark-light': '#6c727a', |
||||
'color-gray': '#9ea2a8', |
||||
'color-gray-medium': '#cbced1', |
||||
'color-gray-light': '#e1e5e8', |
||||
'color-gray-lightest': '#f2f3f5', |
||||
'color-black': '#000000', |
||||
'color-white': '#ffffff', |
||||
}; |
||||
|
||||
const getStyleTag = () => { |
||||
const style = document.getElementById('sidebar-style'); |
||||
if (style) { |
||||
return style; |
||||
} |
||||
const newElement = document.createElement('style'); |
||||
|
||||
newElement.id = 'sidebar-style'; |
||||
newElement.setAttribute('type', 'text/css'); |
||||
|
||||
document.head.appendChild(newElement); |
||||
return newElement; |
||||
}; |
||||
|
||||
function lightenDarkenColor(col, amt) { |
||||
let usePound = false; |
||||
|
||||
if (col[0] === '#') { |
||||
col = col.slice(1); |
||||
usePound = true; |
||||
} |
||||
|
||||
const num = parseInt(col, 16); |
||||
|
||||
let r = (num >> 16) + amt; |
||||
|
||||
if (r > 255) { r = 255; } else if (r < 0) { r = 0; } |
||||
|
||||
let b = ((num >> 8) & 0x00FF) + amt; |
||||
|
||||
if (b > 255) { b = 255; } else if (b < 0) { b = 0; } |
||||
|
||||
let g = (num & 0x0000FF) + amt; |
||||
|
||||
if (g > 255) { g = 255; } else if (g < 0) { g = 0; } |
||||
|
||||
return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16); |
||||
} |
||||
|
||||
function h2r(hex = '', a) { |
||||
const [hash, r, g, b] = hex.match(/#([0-f]{2})([0-f]{2})([0-f]{2})/i) || []; |
||||
|
||||
return hash ? `rgba(${ [r, g, b].map((value) => parseInt(value, 16)).join() }, ${ a })` : hex; |
||||
} |
||||
|
||||
const modifier = '.sidebar--custom-colors'; |
||||
|
||||
const query = { _id: /theme-color-rc/ }; |
||||
const useTheme = () => { |
||||
const customColors = useSettings(query); |
||||
const result = useMemo(() => { |
||||
const n900 = customColors.find(({ _id }) => _id === 'theme-color-rc-color-primary-darkest'); |
||||
const n800 = customColors.find(({ _id }) => _id === 'theme-color-rc-color-primary-dark'); |
||||
const sibebarSurface = customColors.find(({ _id }) => _id === 'theme-color-rc-color-primary-background'); |
||||
const n700 = customColors.find(({ _id }) => _id === ''); |
||||
const n600 = customColors.find(({ _id }) => _id === 'theme-color-rc-color-primary-light'); |
||||
const n500 = customColors.find(({ _id }) => _id === 'theme-color-rc-primary-light-medium'); |
||||
|
||||
const n400 = customColors.find(({ _id }) => _id === ''); |
||||
const n300 = customColors.find(({ _id }) => _id === ''); |
||||
|
||||
const n200 = customColors.find(({ _id }) => _id === 'theme-color-rc-color-primary-lightest'); |
||||
const n100 = customColors.find(({ _id }) => _id === ''); |
||||
return { |
||||
...colors, |
||||
...n900 && { n900: n900.value }, |
||||
...n800 && { n800: n800.value }, |
||||
...(sibebarSurface || n800) && { sibebarSurface: sibebarSurface.value || n800.value }, |
||||
...(n700?.value[0] === '#' || n800?.value[0] === '#') && { n700: n700?.value || lightenDarkenColor(n800.value, 10) }, |
||||
...n700 && { n700: n700.value }, |
||||
...n600 && { n600: n600.value }, |
||||
...n500 && { n500: n500.value }, |
||||
|
||||
...n400 && { n400: n400.value }, |
||||
...n300 && { n300: n300.value }, |
||||
|
||||
...n200 && { n200: n200.value }, |
||||
...n100 && { n100: n100.value }, |
||||
}; |
||||
}, [customColors]); |
||||
return result; |
||||
}; |
||||
|
||||
const toVar = (color) => (color[0] === '#' ? color : oldPallet[color] || `var(--${ color })`); |
||||
|
||||
const getStyle = ((selector) => (colors) => ` |
||||
${ selector } { |
||||
--rcx-color-neutral-100: ${ toVar(colors.n900) }; |
||||
--rcx-color-neutral-200: ${ toVar(colors.n800) }; |
||||
--rcx-color-neutral-300: ${ toVar(colors.n700) }; |
||||
--rcx-color-neutral-400: ${ toVar(colors.n600) }; |
||||
--rcx-color-neutral-500: ${ toVar(colors.n500) }; |
||||
--rcx-color-neutral-600: ${ toVar(colors.n400) }; |
||||
--rcx-color-neutral-700: ${ toVar(colors.n300) }; |
||||
--rcx-color-neutral-800: ${ toVar(colors.n200) }; |
||||
--rcx-color-neutral-900: ${ toVar(colors.n100) }; |
||||
|
||||
--rcx-color-primary-100: ${ toVar(colors.b900) }; |
||||
--rcx-color-primary-200: ${ toVar(colors.b800) }; |
||||
--rcx-color-primary-300: ${ toVar(colors.b700) }; |
||||
--rcx-color-primary-400: ${ toVar(colors.b600) }; |
||||
--rcx-color-primary-500: ${ toVar(colors.b500) }; |
||||
--rcx-color-primary-600: ${ toVar(colors.b400) }; |
||||
--rcx-color-primary-700: ${ toVar(colors.b300) }; |
||||
--rcx-color-primary-800: ${ toVar(colors.b200) }; |
||||
--rcx-color-primary-900: ${ toVar(colors.b100) }; |
||||
|
||||
--rcx-button-colors-secondary-active-border-color: ${ toVar(colors.n900) }; |
||||
--rcx-button-colors-secondary-active-background-color: ${ toVar(colors.n800) }; |
||||
--rcx-button-colors-secondary-color: ${ toVar(colors.n600) }; |
||||
--rcx-button-colors-secondary-border-color: ${ toVar(colors.n800) }; |
||||
--rcx-button-colors-secondary-background-color: ${ toVar(colors.n800) }; |
||||
--rcx-button-colors-secondary-hover-background-color: ${ toVar(colors.n900) }; |
||||
--rcx-button-colors-secondary-hover-border-color: ${ toVar(colors.n900) }; |
||||
--rcx-sidebar-item-background-color-hover: ${ toVar(colors.n900) }; |
||||
--rcx-sidebar-item-background-color-selected: ${ h2r(toVar(colors.n700 || colors.n800), 0.3) }; |
||||
--rcx-tag-colors-ghost-background-color: ${ toVar(colors.n700) }; |
||||
--rcx-color-surface: ${ toVar(colors.n900) }; |
||||
|
||||
--rcx-divider-color: ${ h2r(toVar(colors.n900), 0.4) }; |
||||
--rcx-color-foreground-alternative: ${ toVar(colors.n100) }; |
||||
--rcx-color-foreground-hint: ${ toVar(colors.n600) }; |
||||
|
||||
} |
||||
.rcx-sidebar { |
||||
background-color: ${ toVar(colors.sibebarSurface) }; |
||||
} |
||||
`)(isInternetExplorer11 ? ':root' : modifier);
|
||||
|
||||
const useSidebarPaletteColorIE11 = () => { |
||||
const colors = useTheme(); |
||||
useEffect(() => { |
||||
(async () => { |
||||
const [{ default: cssVars }, CSSOM] = await Promise.all([import('css-vars-ponyfill'), import('cssom')]); |
||||
try { |
||||
getStyleTag().innerHTML = getStyle(colors); |
||||
const fuselageStyle = document.getElementById('fuselage-style'); |
||||
|
||||
if (!fuselageStyle) { |
||||
return; |
||||
} |
||||
|
||||
const sidebarStyle = fuselageStyle.cloneNode(true); |
||||
sidebarStyle.setAttribute('id', 'sidebar-modifier'); |
||||
document.head.appendChild(sidebarStyle); |
||||
|
||||
const fuselageStyleRules = sidebarStyle.innerText.match(/(.|\n)*?\{((.|\n)*?)\}(.|\n)*?/gi).filter((text) => /\.rcx-(sidebar|button|divider|input)/.test(text) && /(color|background|shadow)/.test(text)); |
||||
|
||||
const sheet = CSSOM.parse(fuselageStyleRules.join(' ')); |
||||
|
||||
|
||||
const filterSelectors = (selector) => /rcx-(sidebar|button|divider|input)/.test(selector); |
||||
const insertSelector = (selector) => selector.replace(/^((html:not\(\.js-focus-visible\)|\.js-focus-visible)|\.)(.*)/, (match, group, g2, g3, offset, text) => { |
||||
if (group === '.') { |
||||
return `${ modifier } ${ text }`; |
||||
} |
||||
return `${ match } ${ modifier } ${ g3 }`; |
||||
}); |
||||
|
||||
sidebarStyle.innerHTML = sheet.cssRules.map((rule) => { |
||||
rule.selectorText = rule.selectorText.split(/,[ \n]/).filter(filterSelectors).map(insertSelector).join(); |
||||
Array.from(rule.style.length).map((_, index) => rule.style[index]).forEach((key, index) => !/color|background|shadow/.test(key) && rule.style.removeProperty(rule.style[index])); |
||||
return rule.cssText; |
||||
}).join(''); |
||||
cssVars({ |
||||
include: 'style#sidebar-style,style#sidebar-modifier', |
||||
onlyLegacy: false, |
||||
preserveStatic: true, |
||||
// preserveVars: true,
|
||||
silent: true, |
||||
}); |
||||
} catch (error) { |
||||
console.log(error); |
||||
} |
||||
})(); |
||||
return () => { |
||||
getStyleTag().remove(); |
||||
}; |
||||
}, [colors]); |
||||
}; |
||||
|
||||
export const useSidebarPaletteColor = isInternetExplorer11 ? useSidebarPaletteColorIE11 : () => { |
||||
const colors = useTheme(); |
||||
useLayoutEffect(() => { |
||||
getStyleTag().innerHTML = getStyle(colors); |
||||
|
||||
return () => { |
||||
getStyleTag().innerHTML = ''; |
||||
}; |
||||
}, [colors]); |
||||
}; |
@ -0,0 +1,21 @@ |
||||
import { useMemo } from 'react'; |
||||
|
||||
import { useUserPreference } from '../../contexts/UserContext'; |
||||
import Condensed from '../Item/Condensed'; |
||||
import Extended from '../Item/Extended'; |
||||
import Medium from '../Item/Medium'; |
||||
|
||||
export const useTemplateByViewMode = () => { |
||||
const sidebarViewMode = useUserPreference('sidebarViewMode'); |
||||
return useMemo(() => { |
||||
switch (sidebarViewMode) { |
||||
case 'extended': |
||||
return Extended; |
||||
case 'medium': |
||||
return Medium; |
||||
case 'condensed': |
||||
default: |
||||
return Condensed; |
||||
} |
||||
}, [sidebarViewMode]); |
||||
}; |
@ -0,0 +1,290 @@ |
||||
import React, { useState, useMemo, useEffect, useRef } from 'react'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Sidebar, TextInput, Box, Icon } from '@rocket.chat/fuselage'; |
||||
import { useMutableCallback, useDebouncedValue, useStableArray, useResizeObserver, useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; |
||||
import memoize from 'memoize-one'; |
||||
import { css } from '@rocket.chat/css-in-js'; |
||||
import { FixedSizeList as List } from 'react-window'; |
||||
import tinykeys from 'tinykeys'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { usePreventDefault } from '../hooks/usePreventDefault'; |
||||
import { useSetting } from '../../contexts/SettingsContext'; |
||||
import { useMethodData, AsyncState } from '../../contexts/ServerContext'; |
||||
import { roomTypes } from '../../../app/utils'; |
||||
import { useUserPreference, useUserSubscriptions } from '../../contexts/UserContext'; |
||||
import { itemSizeMap, SideBarItemTemplateWithData } from '../RoomList'; |
||||
import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; |
||||
import { useAvatarTemplate } from '../hooks/useAvatarTemplate'; |
||||
|
||||
const createItemData = memoize((items, t, SideBarItemTemplate, AvatarTemplate, useRealName, extended) => ({ |
||||
items, |
||||
t, |
||||
SideBarItemTemplate, |
||||
AvatarTemplate, |
||||
useRealName, |
||||
extended, |
||||
})); |
||||
|
||||
const Row = React.memo(({ data, index, style }) => { |
||||
const { items, t, SideBarItemTemplate, AvatarTemplate, useRealName, extended } = data; |
||||
const item = items[index]; |
||||
if (item.t === 'd' && !item.u) { |
||||
return <UserItem id={`search-${ item._id }`} useRealName={useRealName} style={style} t={t} item={item} SideBarItemTemplate={SideBarItemTemplate} AvatarTemplate={AvatarTemplate} />; |
||||
} |
||||
return <SideBarItemTemplateWithData id={`search-${ item._id }`} tabIndex={-1} extended={extended} style={style} t={t} room={item} SideBarItemTemplate={SideBarItemTemplate} AvatarTemplate={AvatarTemplate} />; |
||||
}); |
||||
|
||||
const UserItem = React.memo(({ item, id, style, t, SideBarItemTemplate, AvatarTemplate, useRealName }) => { |
||||
const title = useRealName ? item.fname || item.name : item.name || item.fname; |
||||
const icon = <Sidebar.Item.Icon name={roomTypes.getIcon(item)}/>; |
||||
const href = roomTypes.getRouteLink(item.t, item); |
||||
|
||||
return <SideBarItemTemplate |
||||
is='a' |
||||
id={id} |
||||
href={href} |
||||
title={title} |
||||
subtitle={t('No_messages_yet')} |
||||
avatar={AvatarTemplate && <AvatarTemplate {...item}/>} |
||||
icon={icon} |
||||
style={style} |
||||
/>; |
||||
}); |
||||
|
||||
|
||||
const shortcut = (() => { |
||||
if (!Meteor.Device.isDesktop()) { |
||||
return ''; |
||||
} |
||||
if (window.navigator.platform.toLowerCase().includes('mac')) { |
||||
return '(\u2318+K)'; |
||||
} |
||||
return '(\u2303+K)'; |
||||
})(); |
||||
|
||||
const useSpotlight = (filterText = '', usernames) => { |
||||
const expression = /(@|#)?(.*)/i; |
||||
const [, mention, name] = filterText.match(expression); |
||||
|
||||
const searchForChannels = mention === '#'; |
||||
const searchForDMs = mention === '@'; |
||||
|
||||
const type = useMemo(() => { |
||||
if (searchForChannels) { |
||||
return { users: false, rooms: true }; |
||||
} |
||||
if (searchForDMs) { |
||||
return { users: true, rooms: false }; |
||||
} |
||||
return { users: true, rooms: true }; |
||||
}, [searchForChannels, searchForDMs]); |
||||
const args = useMemo(() => [name, usernames, type], [type, name, usernames]); |
||||
|
||||
const [data = { users: [], rooms: [] }, status] = useMethodData('spotlight', args); |
||||
|
||||
return useMemo(() => { |
||||
if (!data) { |
||||
return { data: { users: [], rooms: [] }, status: AsyncState.LOADING }; |
||||
} |
||||
return { data, status }; |
||||
}, [data]); |
||||
}; |
||||
|
||||
const options = { |
||||
sort: { |
||||
lm: -1, |
||||
name: 1, |
||||
}, |
||||
}; |
||||
|
||||
const useSearchItems = (filterText) => { |
||||
const expression = /(@|#)?(.*)/i; |
||||
const teste = filterText.match(expression); |
||||
|
||||
const [, type, name] = teste; |
||||
const query = useMemo(() => { |
||||
const filterRegex = new RegExp(RegExp.escape(name), 'i'); |
||||
|
||||
return { |
||||
$or: [ |
||||
{ name: filterRegex }, |
||||
{ fname: filterRegex }, |
||||
], |
||||
...type && { |
||||
t: type === '@' ? 'd' : { $ne: 'd' }, |
||||
}, |
||||
}; |
||||
}, [name, type]); |
||||
|
||||
const localRooms = useUserSubscriptions(query, options); |
||||
|
||||
const usernamesFromClient = useStableArray([...localRooms?.map(({ t, name }) => (t === 'd' ? name : null))].filter(Boolean)); |
||||
|
||||
const { data: spotlight, status } = useSpotlight(filterText, usernamesFromClient); |
||||
|
||||
return useMemo(() => { |
||||
const resultsFromServer = []; |
||||
|
||||
const filterUsersUnique = ({ _id }, index, arr) => index === arr.findIndex((user) => _id === user._id); |
||||
const roomFilter = (room) => !localRooms.find((item) => (room.t === 'd' && room.uids.length > 1 && room.uids.includes(item._id)) || [item.rid, item._id].includes(room._id)); |
||||
const usersfilter = (user) => !localRooms.find((room) => room.t !== 'd' || (room.uids.length === 2 && room.uids.includes(user._id))); |
||||
|
||||
const userMap = (user) => ({ |
||||
_id: user._id, |
||||
t: 'd', |
||||
name: user.username, |
||||
fname: user.name, |
||||
avatarETag: user.avatarETag, |
||||
}); |
||||
|
||||
const exact = resultsFromServer.filter((item) => [item.usernamame, item.name, item.fname].includes(name)); |
||||
|
||||
resultsFromServer.push(...spotlight.users.filter(filterUsersUnique).filter(usersfilter).map(userMap)); |
||||
resultsFromServer.push(...spotlight.rooms.filter(roomFilter)); |
||||
|
||||
return { data: Array.from(new Set([...exact, ...localRooms, ...resultsFromServer])), status }; |
||||
}, [localRooms, name, spotlight]); |
||||
}; |
||||
|
||||
const useInput = (initial) => { |
||||
const [value, setValue] = useState(initial); |
||||
const onChange = useMutableCallback((e) => { |
||||
setValue(e.currentTarget.value); |
||||
}); |
||||
return { value, onChange, setValue }; |
||||
}; |
||||
|
||||
const toggleSelectionState = (next, current, input) => { |
||||
input.setAttribute('aria-activedescendant', next.id); |
||||
next.setAttribute('aria-selected', true); |
||||
next.classList.add('rcx-sidebar-item--selected'); |
||||
if (current) { |
||||
current.setAttribute('aria-selected', false); |
||||
current.classList.remove('rcx-sidebar-item--selected'); |
||||
} |
||||
}; |
||||
|
||||
const SearchList = React.forwardRef(function SearchList({ onClose }, ref) { |
||||
const listId = useUniqueId(); |
||||
const t = useTranslation(); |
||||
const { setValue: setFilterValue, ...filter } = useInput(''); |
||||
|
||||
const autofocus = useAutoFocus(); |
||||
|
||||
const listRef = useRef(); |
||||
|
||||
const selectedElement = useRef(); |
||||
const itemIndexRef = useRef(0); |
||||
|
||||
const sidebarViewMode = useUserPreference('sidebarViewMode'); |
||||
const showRealName = useSetting('UI_Use_Real_Name'); |
||||
|
||||
const sideBarItemTemplate = useTemplateByViewMode(); |
||||
const avatarTemplate = useAvatarTemplate(); |
||||
|
||||
const itemSize = itemSizeMap(sidebarViewMode); |
||||
|
||||
const extended = sidebarViewMode === 'extended'; |
||||
|
||||
const filterText = useDebouncedValue(filter.value, 100); |
||||
|
||||
const placeholder = [t('Search'), shortcut].filter(Boolean).join(' '); |
||||
|
||||
const { data: items, status } = useSearchItems(filterText); |
||||
|
||||
const itemData = createItemData(items, t, sideBarItemTemplate, avatarTemplate, showRealName, extended); |
||||
|
||||
const { ref: boxRef, contentBoxSize: { blockSize = 750 } = {} } = useResizeObserver({ debounceDelay: 100 }); |
||||
|
||||
usePreventDefault(boxRef); |
||||
|
||||
const changeSelection = useMutableCallback((dir) => { |
||||
let nextSelectedElement = null; |
||||
|
||||
if (dir === 'up') { |
||||
nextSelectedElement = selectedElement.current.previousSibling; |
||||
} else { |
||||
nextSelectedElement = selectedElement.current.nextSibling; |
||||
} |
||||
|
||||
if (nextSelectedElement) { |
||||
toggleSelectionState(nextSelectedElement, selectedElement.current, autofocus.current); |
||||
return nextSelectedElement; |
||||
} |
||||
return selectedElement.current; |
||||
}); |
||||
|
||||
const resetCursor = useMutableCallback(() => { |
||||
itemIndexRef.current = 0; |
||||
listRef.current.scrollToItem(itemIndexRef.current); |
||||
selectedElement.current = boxRef.current.querySelector('a.rcx-sidebar-item'); |
||||
if (selectedElement.current) { |
||||
toggleSelectionState(selectedElement.current, undefined, autofocus.current); |
||||
} |
||||
}); |
||||
|
||||
useEffect(() => { |
||||
resetCursor(); |
||||
}, [filterText, resetCursor]); |
||||
|
||||
useEffect(() => { |
||||
if (!autofocus.current) { |
||||
return; |
||||
} |
||||
const unsubscribe = tinykeys(autofocus.current, { |
||||
Escape: (event) => { |
||||
event.preventDefault(); |
||||
setFilterValue((value) => { |
||||
if (!value) { |
||||
onClose(); |
||||
} |
||||
resetCursor(); |
||||
return ''; |
||||
}); |
||||
}, |
||||
Tab: onClose, |
||||
ArrowUp: () => { |
||||
itemIndexRef.current = Math.max(itemIndexRef.current - 1, 0); |
||||
listRef.current.scrollToItem(itemIndexRef.current); |
||||
const currentElement = changeSelection('up'); |
||||
selectedElement.current = currentElement; |
||||
}, |
||||
ArrowDown: () => { |
||||
const currentElement = changeSelection('down'); |
||||
selectedElement.current = currentElement; |
||||
itemIndexRef.current = Math.min(itemIndexRef.current + 1, items?.length + 1); |
||||
listRef.current.scrollToItem(itemIndexRef.current); |
||||
}, |
||||
Enter: () => { |
||||
if (selectedElement.current) { |
||||
selectedElement.current.click(); |
||||
} |
||||
}, |
||||
}); |
||||
return () => { |
||||
unsubscribe(); |
||||
}; |
||||
}, [autofocus, changeSelection, items.length, onClose, resetCursor, setFilterValue]); |
||||
|
||||
return <Box position='absolute' rcx-sidebar h='full' display='flex' flexDirection='column' zIndex={99} w='full' className={css`left: 0; top: 0;`} ref={ref}> |
||||
<Sidebar.TopBar.Section role='search' is='form'> |
||||
<TextInput aria-owns={listId} data-qa='sidebar-search-input' ref={autofocus} {...filter} placeholder={placeholder} addon={<Icon name='cross' size='x20' onClick={onClose}/>}/> |
||||
</Sidebar.TopBar.Section> |
||||
<Box aria-expanded='true' role='listbox' id={listId} tabIndex={-1} flexShrink={1} h='full' w='full' ref={boxRef} data-qa='sidebar-search-result' onClick={onClose} aria-busy={status !== AsyncState.DONE}> |
||||
<List |
||||
height={blockSize} |
||||
itemCount={items?.length} |
||||
itemSize={itemSize} |
||||
itemData={itemData} |
||||
overscanCount={25} |
||||
width='100%' |
||||
ref={listRef} |
||||
> |
||||
{Row} |
||||
</List> |
||||
</Box> |
||||
</Box>; |
||||
}); |
||||
|
||||
export default SearchList; |
@ -0,0 +1,32 @@ |
||||
import React from 'react'; |
||||
import { Sidebar } from '@rocket.chat/fuselage'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { useMethod } from '../../contexts/ServerContext'; |
||||
import { useOmnichannelShowQueueLink, useOmnichannelAgentAvailable, useOmnichannelQueueLink } from '../../contexts/OmnichannelContext'; |
||||
|
||||
const OmnichannelSection = React.memo((props) => { |
||||
const method = useMethod('livechat:changeLivechatStatus'); |
||||
const t = useTranslation(); |
||||
const agentAvailable = useOmnichannelAgentAvailable(); |
||||
const showOmnichannelQueueLink = useOmnichannelShowQueueLink(); |
||||
const queueLink = useOmnichannelQueueLink(); |
||||
|
||||
const icon = { |
||||
title: agentAvailable ? t('Available') : t('Not_Available'), |
||||
icon: agentAvailable ? 'message' : 'message-disabled', |
||||
...agentAvailable && { success: 1 }, |
||||
}; |
||||
|
||||
return <Sidebar.TopBar.ToolBox { ...props }> |
||||
<Sidebar.TopBar.Title>{t('Omnichannel')}</Sidebar.TopBar.Title> |
||||
<Sidebar.TopBar.Actions> |
||||
{showOmnichannelQueueLink && <Sidebar.TopBar.Action icon='queue' title={t('Queue')} is='a' href={queueLink}/> } |
||||
<Sidebar.TopBar.Action {...icon} onClick={() => { method(); }}/> |
||||
</Sidebar.TopBar.Actions> |
||||
</Sidebar.TopBar.ToolBox>; |
||||
}); |
||||
|
||||
export default OmnichannelSection; |
||||
|
||||
OmnichannelSection.size = 56; |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue