[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
Guilherme Gazzo 5 years ago committed by GitHub
parent c54ab5fd23
commit edda3511c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .storybook/mocks/meteor.js
  2. 2
      .storybook/webpack.config.js
  3. 1
      app/discussion/client/index.js
  4. 12
      app/discussion/client/views/DiscussionList.html
  5. 33
      app/discussion/client/views/DiscussionList.js
  6. 11
      app/discussion/lib/discussionRoomType.js
  7. 1
      app/livechat/client/index.js
  8. 82
      app/livechat/client/lib/stream/queueManager.js
  9. 503
      app/livechat/client/stylesheets/livechat.css
  10. 36
      app/livechat/client/views/sideNav/livechat.html
  11. 142
      app/livechat/client/views/sideNav/livechat.js
  12. 2
      app/theme/client/imports/components/messages.css
  13. 11
      app/theme/client/imports/components/sidebar/rooms-list.css
  14. 151
      app/theme/client/imports/components/sidebar/sidebar-header.css
  15. 334
      app/theme/client/imports/components/sidebar/sidebar-item.css
  16. 25
      app/theme/client/imports/components/sidebar/sidebar.css
  17. 123
      app/theme/client/imports/components/sidebar/toolbar.css
  18. 180
      app/theme/client/imports/general/base_old.css
  19. 8
      app/theme/client/imports/general/rtl.css
  20. 11
      app/theme/client/imports/general/theme_old.css
  21. 3
      app/theme/client/main.css
  22. 2
      app/ui-cached-collection/client/models/CachedCollection.js
  23. 8
      app/ui-master/client/main.js
  24. 1
      app/ui-master/server/inject.js
  25. 2
      app/ui-message/client/popup/messagePopup.js
  26. 2
      app/ui-message/client/popup/messagePopupSlashCommandPreview.js
  27. 3
      app/ui-sidenav/client/chatRoomItem.html
  28. 53
      app/ui-sidenav/client/chatRoomItem.js
  29. 12
      app/ui-sidenav/client/index.js
  30. 16
      app/ui-sidenav/client/roomList.js
  31. 40
      app/ui-sidenav/client/sideNav.html
  32. 5
      app/ui-sidenav/client/sideNav.js
  33. 20
      app/ui-sidenav/client/sidebarHeader.html
  34. 344
      app/ui-sidenav/client/sidebarHeader.js
  35. 76
      app/ui-sidenav/client/sidebarItem.html
  36. 238
      app/ui-sidenav/client/sidebarItem.js
  37. 30
      app/ui-sidenav/client/toolbar.html
  38. 203
      app/ui-sidenav/client/toolbar.js
  39. 11
      app/ui-sidenav/client/userPresence.js
  40. 2
      app/ui-utils/client/lib/popover.html
  41. 1
      app/ui/client/index.js
  42. 4
      app/ui/client/lib/Tooltip.js
  43. 8
      app/ui/client/views/app/pageSettingsContainer.html
  44. 2
      client/account/AccountProfileForm.js
  45. 5
      client/admin/users/UserInfo.js
  46. 6
      client/channel/UserCard/index.js
  47. 7
      client/channel/UserInfo/index.js
  48. 4
      client/components/SortList.js
  49. 4
      client/components/basic/Buttons/ActionButton.js
  50. 5
      client/components/basic/Sidebar.js
  51. 11
      client/components/basic/UserCard.js
  52. 62
      client/components/basic/UserStatus.js
  53. 4
      client/components/basic/UserStatusMenu.js
  54. 2
      client/components/basic/avatar/BaseAvatar.js
  55. 4
      client/components/basic/avatar/RoomAvatar.js
  56. 4
      client/components/basic/avatar/UserAvatar.js
  57. 8
      client/components/basic/userStatus/UserStatus.js
  58. 26
      client/contexts/OmnichannelContext.ts
  59. 40
      client/contexts/UserContext.ts
  60. 19
      client/hooks/useOutsideClick.js
  61. 12
      client/hooks/useTimeAgo.js
  62. 81
      client/lib/presence.js
  63. 8
      client/lib/statusColors.ts
  64. 9
      client/providers/MeteorProvider.js
  65. 120
      client/providers/OmniChannelProvider.tsx
  66. 3
      client/providers/SettingsProvider.tsx
  67. 7
      client/providers/UserProvider.tsx
  68. 2
      client/reactAdapters.js
  69. 39
      client/sidebar/Item/Condensed.js
  70. 74
      client/sidebar/Item/Condensed.stories.js
  71. 58
      client/sidebar/Item/Extended.js
  72. 87
      client/sidebar/Item/Extended.stories.js
  73. 13
      client/sidebar/Item/ExtendedSkeleton.js
  74. 41
      client/sidebar/Item/Medium.js
  75. 73
      client/sidebar/Item/Medium.stories.js
  76. 12
      client/sidebar/Item/skeletons/CondensedSkeleton.js
  77. 12
      client/sidebar/Item/skeletons/MediumSkeleton.js
  78. 18
      client/sidebar/Item/skeletons/Skeleton.stories.js
  79. 222
      client/sidebar/RoomList.js
  80. 159
      client/sidebar/RoomMenu.js
  81. 161
      client/sidebar/Sidebar.stories.js
  82. 146
      client/sidebar/header/UserAvatarButton.js
  83. 92
      client/sidebar/header/actions/CreateRoom.js
  84. 14
      client/sidebar/header/actions/Directory.js
  85. 16
      client/sidebar/header/actions/Home.js
  86. 14
      client/sidebar/header/actions/Login.js
  87. 80
      client/sidebar/header/actions/Menu.js
  88. 48
      client/sidebar/header/actions/Search.js
  89. 22
      client/sidebar/header/actions/Sort.js
  90. 36
      client/sidebar/header/index.js
  91. 28
      client/sidebar/hooks/useAvatarTemplate.js
  92. 20
      client/sidebar/hooks/usePreventDefault.js
  93. 19
      client/sidebar/hooks/useQueryOptions.js
  94. 80
      client/sidebar/hooks/useRoomList.ts
  95. 20
      client/sidebar/hooks/useShortcutOpenMenu.js
  96. 232
      client/sidebar/hooks/useSidebarPaletteColor.js
  97. 21
      client/sidebar/hooks/useTemplateByViewMode.js
  98. 290
      client/sidebar/search/SearchList.js
  99. 32
      client/sidebar/sections/Omnichannel.js
  100. 1
      client/startup/i18n.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,4 +1,7 @@
export const Meteor = {
Device: {
isDesktop: () => false,
},
isClient: true,
isServer: false,
_localStorage: window.localStorage,

@ -53,7 +53,7 @@ module.exports = async ({ config }) => {
require.resolve('./mocks/meteor.js'),
),
new webpack.NormalModuleReplacementPlugin(
/\/server(\/index.js)$/,
/(app)\/*.*\/(server)\/*/,
require.resolve('./mocks/empty.js'),
),
);

@ -1,6 +1,5 @@
// Templates
import './views/creationDialog/CreateDiscussion';
import './views/DiscussionList';
import './views/DiscussionTabbar';
// Other UI extensions

@ -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,6 +1,4 @@
import { Meteor } from 'meteor/meteor';
import { RoomTypeConfig, roomTypes, getUserPreference } from '../../utils';
import { RoomTypeConfig, roomTypes } from '../../utils';
export class DiscussionRoomType extends RoomTypeConfig {
constructor() {
@ -9,13 +7,6 @@ export class DiscussionRoomType extends RoomTypeConfig {
order: 25,
label: 'Discussion',
});
// we need a custom template in order to have a custom query showing the subscriptions to discussions
this.customTemplate = 'DiscussionList';
}
condition() {
return getUserPreference(Meteor.userId(), 'sidebarShowDiscussion');
}
}

@ -6,6 +6,5 @@ import './hooks/onCreateRoomTabBar';
import './startup/notifyUnreadRooms';
import './views/app/dialog/closeRoom';
import './stylesheets/livechat.css';
import './views/sideNav/livechat';
import './externalFrame';
import './lib/messageTypes';

@ -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);
};
})();

@ -8,73 +8,6 @@
--color-gray: #9ea2a8;
}
.flex-list {
.active {
background-color: rgba(255, 255, 255, 0.075);
}
}
.trigger-option,
.trigger-value {
display: inline-block;
float: left;
}
.trigger-option {
width: 30%;
max-width: 300px;
padding-right: 4px;
}
.trigger-value {
width: 70%;
textarea,
input,
select {
display: block !important;
width: auto !important;
min-width: 50%;
margin-bottom: 4px;
}
}
.livechat-code {
width: 90%;
max-width: 750px;
text-align: right;
textarea {
display: block;
width: 100%;
height: 200px;
margin-bottom: 1rem;
text-align: left;
background-color: #efefef;
font-family: courier;
font-size: 12px;
}
}
.preview-mode {
width: auto;
margin-bottom: 1em;
}
.livechat-content {
display: flex;
flex-direction: row;
max-width: 960px;
}
.department-agents {
list-style-type: none;
@ -145,10 +78,6 @@
}
}
.livechat-section {
margin-bottom: 22px;
}
.livechat-status {
color: #9d9fa3;
@ -195,14 +124,6 @@
}
}
.rooms-list {
.inquiries {
.opt {
display: none;
}
}
}
.visitor-edit {
h3 {
margin-bottom: 8px;
@ -212,430 +133,6 @@
}
}
.queue-department {
.show-offline {
vertical-align: middle;
}
}
.lc-analytics-table {
display: flex;
height: 100%;
min-height: 300px;
flex-flow: column;
}
.lc-analytics-flex-container {
position: relative;
display: flex;
height: 100%;
flex-flow: row;
}
.lc-analytics-chart-col {
position: relative;
flex: 66.66666%;
max-width: 66.66666%;
max-height: 100%;
padding-right: 2px;
}
.lc-chart-ov-content {
overflow: auto;
}
.lc-analytics-chart-ov-col {
position: relative;
flex: 33.33333%;
max-width: 33.33333%;
height: 100%;
padding-left: 2px;
}
.lc-chart-section {
position: relative;
height: 100%;
max-height: 100%;
}
.lc-chart-section-content {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.border-component-td {
padding: 0.5rem;
}
.lc-analytics-chart-container {
position: relative;
flex: auto;
}
.lc-analytics-header {
float: right;
& > .lc-date-picker-btn {
position: relative;
padding-top: 23px;
color: #383838;
font-size: inherit;
font-weight: inherit;
.fade {
color: #9ea2a8;
}
}
}
.lc-analytics-overview {
display: flex;
margin-top: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
justify-content: center;
}
.lc-analytics-ov-col {
flex: 25%;
max-width: 25%;
min-height: 20px;
&.-full-width {
max-width: 100%;
}
&:not(:last-child) {
border-right: 1px solid #e9e9e9;
}
}
.lc-analytics-ov-case {
width: 100%;
margin-bottom: 20px;
user-select: text;
text-align: center;
&:first-child {
margin-top: 8px;
}
&.-standalone {
& > .title {
display: inline-block;
margin-top: 1px;
text-transform: none;
font-size: 0.875rem;
font-weight: 300;
}
& > .value {
margin-bottom: 6px;
font-size: 1.75rem;
}
}
& > .title {
display: inline-block;
margin-top: 8px;
text-transform: uppercase;
color: #9ea2a8;
font-size: 0.875rem;
font-weight: 300;
}
& > .value {
display: inline-block;
width: 100%;
text-transform: capitalize;
color: #383838;
font-size: 1.75rem;
font-weight: 400;
line-height: 1;
}
}
@media screen and (max-width: 925px) {
.lc-analytics-ov-case {
& > .title {
font-size: 0.5rem;
}
& > .value {
font-size: 1rem;
}
}
.lc-analytics-flex-container {
flex-flow: column;
}
.lc-analytics-chart-col {
flex: 100%;
max-width: 100%;
max-height: 100%;
padding-right: 0;
padding-bottom: 2px;
}
.lc-analytics-chart-ov-col {
flex: 100%;
max-width: 100%;
max-height: 100%;
padding-top: 2px;
padding-left: 0;
}
}
@media screen and (max-width: 800px) {
.lc-analytics-ov-case {
& > .title {
font-size: 0.875rem;
}
& > .value {
font-size: 1.75rem;
}
}
.lc-analytics-flex-container {
flex-flow: row;
}
.lc-analytics-chart-col {
flex: 66.66666%;
max-width: 66.66666%;
max-height: 100%;
padding-right: 2px;
padding-bottom: 0;
}
.lc-analytics-chart-ov-col {
flex: 33.33333%;
max-width: 33.33333%;
max-height: 100%;
padding-top: 0;
padding-left: 2px;
}
}
@media screen and (max-width: 600px) {
.lc-analytics-ov-case {
& > .title {
font-size: 0.5rem;
}
& > .value {
font-size: 1rem;
}
}
.lc-analytics-flex-container {
flex-flow: column;
}
.lc-analytics-chart-col {
flex: 100%;
max-width: 100%;
max-height: 100%;
padding-right: 0;
padding-bottom: 2px;
}
.lc-analytics-chart-ov-col {
flex: 100%;
max-width: 100%;
max-height: 100%;
padding-top: 2px;
padding-left: 0;
}
}
.lc-monitoring-flex {
display: flex;
margin-top: 2px !important;
margin-bottom: 2px !important;
flex-wrap: wrap;
}
.lc-monitoring-doughnut-chart {
flex: 33.33333%;
max-width: 33.33333%;
padding: 2px;
}
.lc-monitoring-line-chart {
flex: 66.66666%;
max-width: 66.66666%;
padding: 2px;
& .lc-analytics-ov-col {
flex: 33.3%;
max-width: 33.3%;
min-height: 20px;
&.-full-width {
max-width: 100%;
}
&:not(:last-child) {
border-right: 1px solid #e9e9e9;
}
}
& .lc-analytics-overview {
margin: auto;
}
}
.lc-monitoring-line-chart-full {
flex: 66.66666%;
width: 100%;
padding: 2px;
}
.lc-monitoring-chart-container {
min-height: 250px;
}
@media screen and (max-width: 925px) {
.lc-monitoring-doughnut-chart {
flex: 100%;
max-width: 100%;
}
.lc-monitoring-line-chart {
flex: 100%;
max-width: 100%;
}
.lc-monitoring-line-chart-full {
flex: 100%;
max-width: 100%;
}
}
@media screen and (max-width: 800px) {
.lc-monitoring-doughnut-chart {
flex: 33.33333%;
max-width: 33.33333%;
}
.lc-monitoring-line-chart {
flex: 66.66666%;
max-width: 66.66666%;
}
.lc-monitoring-line-chart-full {
flex: 66.66666%;
width: 66.66666%;
max-width: 100%;
}
}
@media screen and (max-width: 600px) {
.lc-monitoring-doughnut-chart {
flex: 100%;
max-width: 100%;
}
.lc-monitoring-line-chart {
flex: 100%;
max-width: 100%;
}
.lc-monitoring-line-chart-full {
flex: 100%;
max-width: 100%;
}
}
.livechat-group-filters-wrapper {
display: flex;
}
.livechat-group-filters-container {
flex: 8 0;
}
.livechat-group-filters-buttons {
flex: 0 0 auto;
margin-top: 26px;
padding-left: 30px;
& > .rc-button__group {
margin: 0 5px;
}
}
.livechat-current-chats-tag-filter-wrapper {
display: flex;
& a {
margin-top: auto;
margin-bottom: auto;
}
}
.livechat-current-chats-add-filter-button {
margin-top: 17px;
}
.external-frame {
width: 100%;
height: 100%;

@ -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));
});

@ -152,7 +152,7 @@
.message-body {
&--unstyled {
vertical-align: sub;
vertical-align: bottom;
font: inherit;
line-height: initial;

@ -1,8 +1,10 @@
.rooms-list {
position: relative;
display: flex;
overflow-x: hidden;
overflow-y: auto;
overflow-y: hidden;
flex: 1 1 auto;
@ -45,12 +47,15 @@
&__toolbar-search {
position: absolute;
z-index: 10;
left: 10px;
left: 0;
overflow-y: scroll;
height: 100%;
background-color: var(--sidebar-background);
padding-block-start: 12px;
}
}

@ -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;
}
}

@ -19,20 +19,6 @@
background-color: var(--sidebar-background);
&-light {
position: absolute;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: var(--sidebar-background-light);
}
&-wrap {
position: absolute;
z-index: 1;
@ -115,9 +101,7 @@
@media (width < 780px) {
.sidebar {
position: absolute;
}
.sidebar:not(.sidebar-light) {
user-select: none;
transform: translate3d(-100%, 0, 0);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
@ -151,3 +135,12 @@
transform: translate3d(200%, 0, 0);
}
}
@media (min-width: 1372px) { /* 1440px -68px (eletron menu) */
.sidebar {
flex: 0 0 20%;
width: 20%;
max-width: 20%;
}
}

@ -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;
}
}

@ -648,6 +648,7 @@
}
}
/*
.rc-old #rocket-chat {
position: fixed;
top: 0;
@ -676,175 +677,7 @@
margin-bottom: 0;
padding: 5px;
}
}
.rc-old .account-box {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
& .info {
position: relative;
z-index: 100;
height: 100%;
padding: 10px 0 10px 18px;
& .thumb {
position: relative;
float: left;
width: 42px;
height: 42px;
padding: 0;
&::after {
position: absolute;
z-index: 10;
top: 18px;
left: -14px;
display: block;
width: 8px;
height: 8px;
content: " ";
border-radius: var(--border-radius);
}
}
& .data {
position: relative;
display: flex;
float: left;
width: calc(100% - 60px);
height: 100%;
padding: 0 25px 0 10px;
align-items: center;
flex-flow: row nowrap;
}
& h4 {
position: relative;
display: block;
overflow: hidden;
width: 130px;
margin-top: 3px;
transition: color 0.15s ease-out;
text-align: left;
text-overflow: ellipsis;
font-size: 16px;
font-weight: 400;
line-height: 18px;
}
}
& .options {
position: fixed;
z-index: 99;
top: var(--header-min-height);
left: 0;
overflow-x: hidden;
overflow-y: auto;
width: var(--rooms-box-width);
height: calc(100% - calc(var(--header-min-height) - var(--footer-min-height)));
padding-top: 15px;
transition: transform 0.3s cubic-bezier(0.5, 0, 0.1, 1);
direction: rtl;
-webkit-overflow-scrolling: touch;
&.animated-hidden {
transform: translateY(-100%) translateY(-50px);
}
& > .wrapper {
direction: ltr;
}
& .status {
position: relative;
padding-left: 38px;
&::after {
position: absolute;
z-index: 5;
top: calc(50% - 8px);
left: 18px;
display: block;
width: 13px;
height: 13px;
content: " ";
border-width: 1px;
border-radius: 50%;
}
}
& span.soon {
position: absolute;
top: 17px;
right: -30px;
width: 100px;
font-size: 10px;
}
& i {
display: inline-block;
width: 26px;
margin-left: 0;
text-align: center;
}
& button,
& a {
position: relative;
display: table;
width: 100%;
padding: 15px 12px;
text-decoration: none;
line-height: 1;
&:hover {
text-decoration: none;
}
}
& .icon-logout::before {
margin-right: 0;
}
& .icon-camera::before {
margin-left: 1px;
}
}
}
} */
/* rooms-box */
@ -1056,15 +889,6 @@
}
}
.rc-old .toolbar {
position: absolute;
z-index: 2;
top: var(--header-min-height);
width: 100%;
height: var(--toolbar-height);
}
.rc-old .new-room-highlight a {
animation: highlight 6s infinite;
}

@ -100,14 +100,6 @@
}
}
& .account-box .options {
direction: ltr;
& > .wrapper {
direction: rtl;
}
}
& .flex-tab-container {
border-width: 0 1px 0 0;
}

@ -410,18 +410,11 @@ i.status-online {
background-color: var(--rc-status-online);
}
.account-box .status-online .thumb::after,
.account-box .status.online::after,
.popup-user-status-online,
.status-online::after {
background-color: var(--rc-status-online);
}
.account-box .status-offline .thumb::after,
.account-box .status.offline::after {
background-color: var(--transparent-lighter);
}
i.status-away {
color: var(--rc-status-away);
}
@ -430,8 +423,6 @@ i.status-away {
background-color: var(--rc-status-away);
}
.account-box .status-away .thumb::after,
.account-box .status.away::after,
.popup-user-status-away,
.status-away::after,
.status-pending::after {
@ -446,8 +437,6 @@ i.status-busy {
background-color: var(--rc-status-busy);
}
.account-box .status-busy .thumb::after,
.account-box .status.busy::after,
.popup-user-status-busy,
.status-busy::after {
background-color: var(--rc-status-busy);

@ -22,10 +22,7 @@
/* Sidebar */
@import 'imports/components/sidebar/sidebar.css';
@import 'imports/components/sidebar/sidebar-header.css';
@import 'imports/components/sidebar/sidebar-item.css';
@import 'imports/components/sidebar/sidebar-flex.css';
@import 'imports/components/sidebar/toolbar.css';
@import 'imports/components/sidebar/rooms-list.css';
/* Main */

@ -129,7 +129,7 @@ export class CachedCollection extends EventEmitter {
userRelated = true,
listenChangesForLoggedUsersOnly = false,
useSync = true,
version = 13,
version = 15,
maxCacheTime = 60 * 60 * 24 * 30,
onSyncData = (/* action, record */) => {},
}) {

@ -10,7 +10,6 @@ import { Template } from 'meteor/templating';
import { t, getUserPreference } from '../../utils/client';
import { chatMessages } from '../../ui';
import { mainReady, Layout, iframeLogin, modal, popover, menu, fireGlobalEvent, RoomManager } from '../../ui-utils';
import { toolbarSearch } from '../../ui-sidenav';
import { settings } from '../../settings';
import { CachedChatSubscription, Roles, ChatSubscription, Users } from '../../models';
import { CachedCollectionManager } from '../../ui-cached-collection';
@ -36,11 +35,6 @@ Template.body.onRendered(function() {
new Clipboard('.clipboard');
$(document.body).on('keydown', function(e) {
if ((e.keyCode === 80 || e.keyCode === 75) && (e.ctrlKey === true || e.metaKey === true) && e.shiftKey === false) {
e.preventDefault();
e.stopPropagation();
toolbarSearch.show(true);
}
const unread = Session.get('unread');
if (e.keyCode === 27 && (e.shiftKey === true || e.ctrlKey === true) && (unread != null) && unread !== '') {
e.preventDefault();
@ -120,7 +114,7 @@ Template.body.onRendered(function() {
}
});
Tracker.autorun(function(c) {
this.autorun(function(c) {
const w = window;
const d = document;
const script = 'script';

@ -41,7 +41,6 @@ Meteor.startup(() => {
<style>
body, body * {
animation: none !important;
transition: none !important;
}
</style>
<script>

@ -6,7 +6,6 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { toolbarSearch } from '../../../ui-sidenav';
import './messagePopup.html';
const keys = {
@ -150,7 +149,6 @@ Template.messagePopup.onCreated(function() {
template.onInputKeyup = (event) => {
if (template.closeOnEsc === true && template.open.curValue === true && event.which === keys.ESC) {
template.open.set(false);
toolbarSearch.close();
event.preventDefault();
event.stopPropagation();
return;

@ -5,7 +5,6 @@ import { Template } from 'meteor/templating';
import { slashCommands } from '../../../utils';
import { hasAtLeastOnePermission } from '../../../authorization';
import { toolbarSearch } from '../../../ui-sidenav';
import './messagePopupSlashCommandPreview.html';
const keys = {
@ -179,7 +178,6 @@ Template.messagePopupSlashCommandPreview.onCreated(function() {
template.onInputKeyup = (event) => {
if (template.open.curValue === true && event.which === keys.ESC) {
template.open.set(false);
toolbarSearch.close();
event.preventDefault();
event.stopPropagation();
return;

@ -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,17 +1,5 @@
import './chatRoomItem.html';
import './sidebarHeader.html';
import './sidebarItem.html';
import './sideNav.html';
import './toolbar.html';
import './roomList.html';
import './chatRoomItem';
import { toolbarSearch } from './sidebarHeader';
import './sidebarItem';
import './sideNav';
import './roomList';
import './toolbar';
import './userPresence';
export {
toolbarSearch,
};

@ -146,7 +146,10 @@ const mergeSubRoom = (subscription) => {
fields: {
lm: 1,
lastMessage: 1,
uids: 1,
v: 1,
streamingOptions: 1,
usernames: 1,
},
};
@ -154,6 +157,16 @@ const mergeSubRoom = (subscription) => {
const lastRoomUpdate = room.lm || subscription.ts || subscription._updatedAt;
if (room.uids) {
subscription.uids = room.uids;
}
if (room.v) {
subscription.v = room.v;
}
subscription.usernames = room.usernames;
subscription.lastMessage = room.lastMessage;
subscription.lm = subscription.lr ? new Date(Math.max(subscription.lr, lastRoomUpdate)) : lastRoomUpdate;
subscription.streamingOptions = room.streamingOptions;
@ -171,6 +184,9 @@ const mergeRoomSub = (room) => {
rid: room._id,
}, {
$set: {
...Array.isArray(room.uids) && { uids: room.uids },
...Array.isArray(room.uids) && { usernames: room.usernames },
...room.v && { v: room.v },
lastMessage: room.lastMessage,
streamingOptions: room.streamingOptions,
...getLowerCaseNames(room, sub.name, sub.fname),

@ -1,32 +1,22 @@
<template name="sideNav">
<aside class="sidebar sidebar--{{sidebarViewMode}} {{#if sidebarHideAvatar}}sidebar--hide-avatar{{/if}}" role="navigation">
<aside class="rcx-sidebar sidebar sidebar--main sidebar--{{sidebarViewMode}} {{#if sidebarHideAvatar}}sidebar--hide-avatar{{/if}}" role="navigation">
{{> sidebarHeader }}
{{#if loggedInUser}}
<div class="wrapper-unread">
<div class="unread-rooms background-primary-action-color color-primary-action-contrast top-unread-rooms hidden">
<i class="icon-up-big"></i> {{_ "More_unreads"}}
</div>
<div class="wrapper-unread">
<div class="unread-rooms background-primary-action-color color-primary-action-contrast top-unread-rooms hidden">
<i class="icon-up-big"></i> {{_ "More_unreads"}}
</div>
<div class="rooms-list" aria-label="{{_ "Channels"}}" role="region">
{{#each roomType}}
{{> Template.dynamic template=template data=data }}
{{/each}}
</div>
<div class="rooms-list sidebar--custom-colors" aria-label="{{_ "Channels"}}" role="region">
{{> sidebarChats }}
</div>
<div class="wrapper-unread">
<div class="unread-rooms background-primary-action-color color-primary-action-contrast bottom-unread-rooms hidden">
<i class="icon-down-big"></i> {{_ "More_unreads"}}
</div>
<div class="wrapper-unread">
<div class="unread-rooms background-primary-action-color color-primary-action-contrast bottom-unread-rooms hidden">
<i class="icon-down-big"></i> {{_ "More_unreads"}}
</div>
</div>
<div class="flex-nav animated-hidden">
{{> Template.dynamic template=flexTemplate data=flexData }}
</div>
{{else}}
<div class="rooms-list" aria-label="{{_ "Channels"}}" role="region">
<div class="wrapper">
{{> roomList anonymous=true label="Channels" }}
</div>
</div>
{{/if}}
</div>
<div class="flex-nav animated-hidden">
{{> Template.dynamic template=flexTemplate data=flexData }}
</div>
<footer class="sidebar__footer">{{{footer}}}</footer>
</aside>

@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { HTML } from 'meteor/htmljs';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
@ -8,6 +9,10 @@ import { SideNav, menu } from '../../ui-utils';
import { settings } from '../../settings';
import { roomTypes, getUserPreference } from '../../utils';
import { Users } from '../../models';
import { createTemplateForComponent } from '../../../client/reactAdapters';
createTemplateForComponent('sidebarHeader', () => import('../../../client/sidebar/header'));
createTemplateForComponent('sidebarChats', () => import('../../../client/sidebar/RoomList'), { renderContainerView: () => HTML.DIV({ style: 'display: flex; flex: 1 1 auto;' }) });// eslint-disable-line new-cap
Template.sideNav.helpers({
flexTemplate() {

@ -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());
});

@ -7,6 +7,7 @@ import mem from 'mem';
import { APIClient } from '../../utils/client';
import { saveUser, interestedUserIds } from '../../../imports/startup/client/listenActiveUsers';
import { Presence } from '../../../client/lib/presence';
import './userPresence.html';
@ -42,9 +43,9 @@ const getAll = _.debounce(async function getAll() {
reject();
});
}
}, 1000);
}, 100);
const get = mem(function get(id) {
export const get = mem(function get(id) {
interestedUserIds.add(id);
const promise = pending.get(id) || new Promise((resolve, reject) => {
promises.set(id, { resolve, reject });
@ -79,11 +80,14 @@ Tracker.autorun(() => {
const isConnected = Meteor.status().connected;
if (!Meteor.userId() || (wasConnected && !isConnected)) {
wasConnected = isConnected;
Presence.reset();
return Meteor.users.update({ status: { $exists: true } }, { $unset: { status: true } }, { multi: true });
}
mem.clear(get);
wasConnected = isConnected;
Presence.emit('restart');
if (featureExists) {
for (const node of data.keys()) {
observer.unobserve(node);
@ -91,9 +95,12 @@ Tracker.autorun(() => {
}
return;
}
getAll();
Accounts.onLogout(() => {
Presence.reset();
interestedUserIds.clear();
});
});

@ -13,7 +13,7 @@
<ul class="rc-popover__list">
{{#each item in group.items}}
{{# with item}}
<li class="rc-popover__item {{#if item.modifier}}rc-popover__item--{{item.modifier}}{{/if}}{{#if hasAction}} js-action{{/if}}" data-type={{item.type}} data-id={{item.id.toLowerCase}} data-href={{item.href}} data-sidenav={{item.sideNav}}>
<li data-qa="{{qa}}" class="rc-popover__item {{#if item.modifier}}rc-popover__item--{{item.modifier}}{{/if}}{{#if hasAction}} js-action{{/if}}" data-type={{item.type}} data-id={{item.id.toLowerCase}} data-href={{item.href}} data-sidenav={{item.sideNav}}>
{{#if item.icon}}
<span class="rc-popover__icon">
{{> icon block="rc-popover__icon-element" icon=item.icon }}

@ -20,7 +20,6 @@ import './views/app/home.html';
import './views/app/notAuthorized.html';
import './views/app/pageContainer.html';
import './views/app/pageCustomContainer.html';
import './views/app/pageSettingsContainer.html';
import './views/app/room.html';
import './views/app/roomSearch.html';
import './views/app/secretURL.html';

@ -8,7 +8,7 @@ let state;
let dom;
let unregister;
const createAchor = () => {
const createAnchor = () => {
const div = document.createElement('div');
div.id = 'react-tooltip';
document.body.appendChild(div);
@ -29,7 +29,7 @@ export const closeTooltip = () => {
};
export const openToolTip = async (title, anchor) => {
dom = dom || createAchor();
dom = dom || createAnchor();
state = {
title,
anchor,

@ -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>

@ -9,7 +9,7 @@ import { useMethod } from '../contexts/ServerContext';
import { getUserEmailAddress } from '../lib/getUserEmailAddress';
import { UserAvatarEditor } from '../components/basic/avatar/UserAvatarEditor';
import CustomFieldsForm from '../components/CustomFieldsForm';
import UserStatusMenu from '../components/basic/userStatus/UserStatusMenu';
import UserStatusMenu from '../components/basic/UserStatusMenu';
const STATUS_TEXT_MAX_LENGTH = 120;

@ -1,3 +1,4 @@
import React, { useMemo } from 'react';
import { Box } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
@ -6,7 +7,7 @@ import { UserInfo } from '../../components/basic/UserInfo';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import { useTranslation } from '../../contexts/TranslationContext';
import { useSetting } from '../../contexts/SettingsContext';
import * as UserStatus from '../../components/basic/UserStatus';
import { UserStatus } from '../../components/basic/UserStatus';
import UserCard from '../../components/basic/UserCard';
import { UserInfoActions } from './UserInfoActions';
import { FormSkeleton } from './Skeleton';
@ -48,7 +49,7 @@ export function UserInfoWithData({ uid, username, ...props }) {
customFields: { ...user.customFields, ...approveManuallyUsers && user.active === false && user.reason && { Reason: user.reason } },
email: getUserEmailAddress(user),
createdAt: user.createdAt,
status: UserStatus.getStatus(status),
status: <UserStatus status={status} />,
customStatus: statusText,
nickname,
};

@ -7,7 +7,7 @@ import { useSetting } from '../../contexts/SettingsContext';
import { useTranslation } from '../../contexts/TranslationContext';
import UserCard from '../../components/basic/UserCard';
import { Backdrop } from '../../components/basic/Backdrop';
import * as UserStatus from '../../components/basic/UserStatus';
import { ReactiveUserStatus } from '../../components/basic/UserStatus';
import { LocalTime } from '../../components/basic/UTCClock';
import { useUserInfoActions, useUserInfoActionsSpread } from '../hooks/useUserInfoActions';
import { useRolesDescription } from '../../contexts/AuthorizationContext';
@ -37,7 +37,7 @@ const UserCardWithData = ({ username, onClose, target, open, rid }) => {
_id,
name = username,
roles = defaultValue,
status,
status = null,
statusText = status,
bio = defaultValue,
utcOffset = defaultValue,
@ -57,7 +57,7 @@ const UserCardWithData = ({ username, onClose, target, open, rid }) => {
localTime: Number.isInteger(utcOffset) && (
<LocalTime utcOffset={utcOffset} />
),
status: UserStatus.getStatus(status),
status: status && <ReactiveUserStatus uid={_id} presence={status} />,
customStatus: statusText,
nickname,
};

@ -8,7 +8,7 @@ import {
} from '../../hooks/useEndpointDataExperimental';
import { useTranslation } from '../../contexts/TranslationContext';
import { useSetting } from '../../contexts/SettingsContext';
import * as UserStatus from '../../components/basic/UserStatus';
import { ReactiveUserStatus } from '../../components/basic/UserStatus';
import UserCard from '../../components/basic/UserCard';
import { FormSkeleton } from '../../admin/users/Skeleton';
import VerticalBar from '../../components/basic/VerticalBar';
@ -33,10 +33,11 @@ export const UserInfoWithData = React.memo(function UserInfoWithData({ uid, user
const user = useMemo(() => {
const { user } = data || { user: {} };
const {
_id,
name,
username,
roles = [],
status,
status = null,
statusText,
bio,
utcOffset,
@ -57,7 +58,7 @@ export const UserInfoWithData = React.memo(function UserInfoWithData({ uid, user
utcOffset,
createdAt: user.createdAt,
// localTime: <LocalTime offset={utcOffset} />,
status: UserStatus.getStatus(status),
status: status && <ReactiveUserStatus uid={_id} presence={status} />,
customStatus: statusText,
nickname,
};

@ -11,9 +11,9 @@ function SortListItem({ text, icon, input }) {
return <Flex.Container>
<Box is='li'>
<Flex.Container>
<Box is='label' componentClassName='rc-popover__label' style={{ width: '100%' }}>
<Box is='label' className='rc-popover__label' style={{ width: '100%' }}>
<Flex.Item grow={0}>
<Box componentClassName='rc-popover__icon'><Icon name={icon} size={20}/></Box>
<Box className='rc-popover__icon'><Icon name={icon} size={20}/></Box>
</Flex.Item>
<Margins inline='x8'>
<Flex.Item grow={1}>

@ -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,10 +1,9 @@
import React, { useMemo } from 'react';
import { css } from '@rocket.chat/css-in-js';
import { Box, Scrollable, Icon } from '@rocket.chat/fuselage';
import { Box, Scrollable, Icon, ActionButton } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoutePath } from '../../contexts/RouterContext';
import { ActionButton } from './Buttons/ActionButton';
const Sidebar = ({ children, ...props }) => <Box display='flex' flexDirection='column' h='full' {...props}>
{children}
@ -19,7 +18,7 @@ const Content = ({ children, ...props }) => <Scrollable {...props}>
const Header = ({ title, onClose, children = undefined, ...props }) => <Box is='header' display='flex' flexDirection='column' pb='x16' {...props}>
{(title || onClose) && <Box display='flex' flexDirection='row' alignItems='center' pi='x24' justifyContent='space-between' flexGrow={1}>
{title && <Box color='neutral-800' fontSize='p1' fontWeight='p1' flexShrink={1} withTruncatedText>{title}</Box>}
{onClose && <ActionButton icon='cross' onClick={onClose}/>}
{onClose && <ActionButton ghost small icon='cross' onClick={onClose}/>}
</Box>}
{children}
</Box>;

@ -1,7 +1,6 @@
import React, { forwardRef } from 'react';
import { Box, Tag, Button, Icon, Skeleton } from '@rocket.chat/fuselage';
import { Box, Tag, ActionButton, Skeleton } from '@rocket.chat/fuselage';
import { ActionButton } from './Buttons/ActionButton';
import UserAvatar from './avatar/UserAvatar';
import * as Status from './UserStatus';
import MarkdownText from './MarkdownText';
@ -14,10 +13,8 @@ const clampStyle = {
wordBreak: 'break-all',
};
export const Action = ({ icon, label, ...props }) => (
<Button title={label} {...props} small mi='x2'>
<Icon name={icon} size='x16' />
</Button>
export const Action = ({ label, ...props }) => (
<ActionButton small title={label} {...props} mi='x2'/>
);
export const Info = (props) => (
@ -90,7 +87,7 @@ const UserCard = forwardRef(({
{ bio && <Info withTruncatedText={false} style={clampStyle} height='x60'><MarkdownText content={bio}/></Info> }
{open && <a onClick={open}>{t('See_full_profile')}</a>}
</Box>
{onClose && <Box><ActionButton icon='cross' onClick={onClose}/></Box>}
{onClose && <Box><ActionButton ghost icon='cross' onClick={onClose}/></Box>}
</UserCardContainer>);

@ -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} />;
});

@ -8,8 +8,8 @@ import {
Box,
} from '@rocket.chat/fuselage';
import { useTranslation } from '../../../contexts/TranslationContext';
import UserStatus from './UserStatus';
import { useTranslation } from '../../contexts/TranslationContext';
import { UserStatus } from './UserStatus';
const UserStatusMenu = ({
onChange = () => {},

@ -8,7 +8,7 @@ function BaseAvatar(props) {
return <Skeleton variant='rect' {...props} />;
}
return <Avatar onError={setError} loading='lazy' {...props}/>;
return <Avatar onError={setError} {...props}/>;
}
export default BaseAvatar;

@ -1,4 +1,4 @@
import React from 'react';
import React, { memo } from 'react';
import BaseAvatar from './BaseAvatar';
import { useRoomAvatarPath } from '../../../contexts/AvatarUrlContext';
@ -9,4 +9,4 @@ function RoomAvatar({ room, ...rest }) {
return <BaseAvatar url={url} {...props}/>;
}
export default RoomAvatar;
export default memo(RoomAvatar);

@ -1,4 +1,4 @@
import React from 'react';
import React, { memo } from 'react';
import BaseAvatar from './BaseAvatar';
import { useUserAvatarPath } from '../../../contexts/AvatarUrlContext';
@ -9,4 +9,4 @@ function UserAvatar({ username, etag, ...rest }) {
return <BaseAvatar url={url} title={username} {...props}/>;
}
export default UserAvatar;
export default memo(UserAvatar);

@ -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;

@ -1,21 +1,37 @@
import { FilterQuery } from 'mongodb';
import { createContext, useContext, useMemo } from 'react';
import { useSubscription, Subscription, Unsubscribe } from 'use-subscription';
import { ISubscription } from '../../definition/ISubscription';
type SubscriptionQuery = {
rid: string | Mongo.ObjectID;
} | {
name: string;
}
} | {
open: boolean;
} | object;
type Fields = {
[key: string]: boolean;
}
type Sort = {
[key: string]: -1 | 1 | number;
}
type FindOptions = {
fields?: Fields;
sort?: Sort;
}
type UserContextValue = {
userId: string | null;
user: Meteor.User | null;
loginWithPassword: (user: string | object, password: string) => Promise<void>;
queryPreference: <T>(key: string | Mongo.ObjectID, defaultValue?: T) => Subscription<T | undefined>;
querySubscription: (query: SubscriptionQuery, fields: Fields) => Subscription <any | null>;
querySubscription: (query: FilterQuery<ISubscription>, fields: Fields, sort?: Sort) => Subscription <ISubscription | undefined>;
querySubscriptions: (query: SubscriptionQuery, options?: FindOptions) => Subscription <Array<ISubscription> | []>;
};
export const UserContext = createContext<UserContextValue>({
@ -30,9 +46,13 @@ export const UserContext = createContext<UserContextValue>({
getCurrentValue: (): undefined => undefined,
subscribe: (): Unsubscribe => (): void => undefined,
}),
querySubscriptions: () => ({
getCurrentValue: (): [] => [],
subscribe: (): Unsubscribe => (): void => undefined,
}),
});
export const useUserId = (): string | Mongo.ObjectID | null =>
export const useUserId = (): string | null =>
useContext(UserContext).userId;
export const useUser = (): Meteor.User | null =>
@ -41,20 +61,26 @@ export const useUser = (): Meteor.User | null =>
export const useLoginWithPassword = (): ((user: string | object, password: string) => Promise<void>) =>
useContext(UserContext).loginWithPassword;
export const useUserPreference = <T>(key: string | Mongo.ObjectID, defaultValue?: T): T | undefined => {
export const useUserPreference = <T>(key: string, defaultValue?: T): T | undefined => {
const { queryPreference } = useContext(UserContext);
const subscription = useMemo(() => queryPreference(key, defaultValue), [queryPreference, key, defaultValue]);
return useSubscription(subscription);
};
export const useUserSubscription = <T>(rid: string | Mongo.ObjectID, fields: Fields): T | undefined => {
export const useUserSubscription = (rid: string, fields: Fields): ISubscription | undefined => {
const { querySubscription } = useContext(UserContext);
const subscription = useMemo(() => querySubscription({ rid }, fields), [querySubscription, rid, fields]);
return useSubscription(subscription);
};
export const useUserSubscriptionByName = <T>(name: string, fields: Fields): T | undefined => {
export const useUserSubscriptions = (query: SubscriptionQuery, options?: FindOptions): Array<ISubscription> | [] => {
const { querySubscriptions } = useContext(UserContext);
const subscription = useMemo(() => querySubscriptions(query, options), [querySubscriptions, query, options]);
return useSubscription(subscription);
};
export const useUserSubscriptionByName = (name: string, fields: Fields, sort?: Sort): ISubscription | undefined => {
const { querySubscription } = useContext(UserContext);
const subscription = useMemo(() => querySubscription({ name }, fields), [querySubscription, name, fields]);
const subscription = useMemo(() => querySubscription({ name }, fields, sort), [querySubscription, name, fields, sort]);
return useSubscription(subscription);
};

@ -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;
}

@ -2,3 +2,15 @@ import { useCallback } from 'react';
import moment from 'moment';
export const useTimeAgo = () => useCallback((time) => moment(time).calendar(null, { sameDay: 'LT', lastWeek: 'dddd LT', sameElse: 'LL' }), []);
export const useShortTimeAgo = () => useCallback((time) => moment(time).calendar(null, {
sameDay: 'LT',
lastDay: '[Yesterday]',
lastWeek: 'dddd',
sameElse(now) {
if (this.isBefore(now, 'year')) {
return 'LL';
}
return 'MMM Do';
},
}), []);

@ -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;

@ -5,6 +5,7 @@ import AvatarUrlProvider from './AvatarUrlProvider';
import ConnectionStatusProvider from './ConnectionStatusProvider';
import CustomSoundProvider from './CustomSoundProvider';
import ModalProvider from './ModalProvider';
import OmniChannelProvider from './OmniChannelProvider';
import RouterProvider from './RouterProvider';
import ServerProvider from './ServerProvider';
import SessionProvider from './SessionProvider';
@ -28,9 +29,11 @@ function MeteorProvider({ children }) {
<CustomSoundProvider>
<UserProvider>
<AuthorizationProvider>
<ModalProvider>
{children}
</ModalProvider>
<OmniChannelProvider>
<ModalProvider>
{children}
</ModalProvider>
</OmniChannelProvider>
</AuthorizationProvider>
</UserProvider>
</CustomSoundProvider>

@ -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;

@ -66,7 +66,8 @@ const SettingsProvider: FunctionComponent<SettingsProviderProps> = ({
const querySettings = useMemo(
() => createReactiveSubscriptionFactory(
(query = {}) => cachedCollection.collection.find({
...('_id' in query) && { _id: { $in: query._id } },
...('_id' in query && Array.isArray(query._id)) && { _id: { $in: query._id } },
...('_id' in query && !Array.isArray(query._id)) && { _id: query._id },
...('group' in query) && { group: query.group },
...('section' in query) && (
query.section

@ -5,7 +5,8 @@ import { getUserPreference } from '../../app/utils/client';
import { UserContext } from '../contexts/UserContext';
import { useReactiveValue } from '../hooks/useReactiveValue';
import { createReactiveSubscriptionFactory } from './createReactiveSubscriptionFactory';
import { Subscriptions } from '../../app/models/client';
import { Subscriptions, Rooms } from '../../app/models/client';
import { ISubscription } from '../../definition/ISubscription';
const getUserId = (): string | null => Meteor.userId();
@ -26,7 +27,6 @@ const loginWithPassword = (user: string | object, password: string): Promise<voi
const UserProvider: FC = ({ children }) => {
const userId = useReactiveValue(getUserId);
const user = useReactiveValue(getUser);
const contextValue = useMemo(() => ({
userId,
user,
@ -34,7 +34,8 @@ const UserProvider: FC = ({ children }) => {
queryPreference: createReactiveSubscriptionFactory(
(key, defaultValue) => getUserPreference(userId, key, defaultValue),
),
querySubscription: createReactiveSubscriptionFactory((query, fields) => Subscriptions.findOne(query, { fields })),
querySubscription: createReactiveSubscriptionFactory<ISubscription | undefined>((query, fields) => Subscriptions.findOne(query, { fields })),
querySubscriptions: createReactiveSubscriptionFactory<Array<ISubscription> | []>((query, options) => (userId ? Subscriptions : Rooms).find(query, options).fetch()),
}), [userId, user]);
return <UserContext.Provider children={children} value={contextValue} />;

@ -122,7 +122,7 @@ export const createTemplateForComponent = (
name,
importFn,
{
renderContainerView = () => HTML.DIV({ style: 'height: 100%;' }), // eslint-disable-line new-cap
renderContainerView = () => HTML.DIV(), // eslint-disable-line new-cap
} = {},
) => {
if (Template[name]) {

@ -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;

@ -57,6 +57,7 @@ Meteor.startup(() => {
return;
}
document.documentElement.classList[isRtl(language) ? 'add' : 'remove']('rtl');
document.documentElement.setAttribute('dir', isRtl(language) ? 'rtl' : 'ltr');
document.querySelector('html').lang = language;
TAPi18n.setLanguage(language);

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save