pull/20349/head^2
gabriellsh 5 years ago committed by GitHub
parent 499b110412
commit b543d0c58a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/api/server/index.js
  2. 21
      app/api/server/lib/rooms.js
  3. 92
      app/api/server/v1/channels.js
  4. 51
      app/api/server/v1/groups.js
  5. 321
      app/api/server/v1/teams.ts
  6. 16
      app/api/server/v1/users.js
  7. 2
      app/apps/client/gameCenter/tabBar.ts
  8. 13
      app/authorization/server/startup.js
  9. 2
      app/autotranslate/client/lib/tabBar.ts
  10. 2
      app/discussion/client/tabBar.ts
  11. 2
      app/e2e/client/tabbar.ts
  12. 6
      app/lib/server/functions/addUserToRoom.js
  13. 10
      app/lib/server/functions/createRoom.js
  14. 5
      app/lib/server/functions/removeUserFromRoom.js
  15. 2
      app/livestream/client/tabBar.tsx
  16. 2
      app/mentions-flextab/client/tabBar.ts
  17. 2
      app/message-pin/client/tabBar.ts
  18. 2
      app/message-snippet/client/tabBar/tabBar.ts
  19. 2
      app/message-star/client/tabBar.ts
  20. 108
      app/models/server/models/Rooms.js
  21. 32
      app/models/server/models/Subscriptions.js
  22. 9
      app/models/server/models/Users.js
  23. 14
      app/models/server/raw/BaseRaw.ts
  24. 137
      app/models/server/raw/Rooms.js
  25. 11
      app/models/server/raw/Subscriptions.ts
  26. 73
      app/models/server/raw/Team.ts
  27. 105
      app/models/server/raw/TeamMember.ts
  28. 9
      app/models/server/raw/Users.js
  29. 2
      app/push-notifications/client/tabBar.ts
  30. 4
      app/statistics/server/lib/statistics.js
  31. 2
      app/threads/client/components/ThreadComponent.tsx
  32. 8
      app/ui-cached-collection/client/models/CachedCollection.js
  33. 2
      app/utils/server/index.js
  34. 31
      app/utils/server/lib/roomTypes.js
  35. 2
      app/videobridge/client/tabBar.tsx
  36. 4
      client/components/Breadcrumbs/index.js
  37. 18
      client/components/GenericModal.stories.js
  38. 77
      client/components/GenericModal.tsx
  39. 5
      client/components/GenericTable/index.js
  40. 4
      client/components/Message/Actions/index.tsx
  41. 9
      client/components/ScrollableContentWrapper.tsx
  42. 14
      client/components/avatar/BaseAvatar.js
  43. 16
      client/components/avatar/BaseAvatar.tsx
  44. 12
      client/components/avatar/UserAvatar.js
  45. 22
      client/components/avatar/UserAvatar.tsx
  46. 46
      client/contexts/ServerContext.ts
  47. 72
      client/contexts/ServerContext/ServerContext.ts
  48. 70
      client/contexts/ServerContext/endpoints.ts
  49. 5
      client/contexts/ServerContext/endpoints/apps/externalComponents.ts
  50. 15
      client/contexts/ServerContext/endpoints/v1/channels/files.ts
  51. 9
      client/contexts/ServerContext/endpoints/v1/chat/followMessage.ts
  52. 15
      client/contexts/ServerContext/endpoints/v1/chat/getDiscussions.ts
  53. 7
      client/contexts/ServerContext/endpoints/v1/chat/getMessage.ts
  54. 16
      client/contexts/ServerContext/endpoints/v1/chat/getThreadsList.ts
  55. 9
      client/contexts/ServerContext/endpoints/v1/chat/unfollowMessage.ts
  56. 3
      client/contexts/ServerContext/endpoints/v1/cloud/manualRegister.ts
  57. 5
      client/contexts/ServerContext/endpoints/v1/custom-user-status/list.ts
  58. 14
      client/contexts/ServerContext/endpoints/v1/emoji-custom/list.ts
  59. 15
      client/contexts/ServerContext/endpoints/v1/groups/files.ts
  60. 15
      client/contexts/ServerContext/endpoints/v1/im/files.ts
  61. 8
      client/contexts/ServerContext/endpoints/v1/livechat/appearance.ts
  62. 10
      client/contexts/ServerContext/endpoints/v1/livechat/visitorInfo.ts
  63. 5
      client/contexts/ServerContext/endpoints/v1/rooms/autocompleteChannelAndPrivate.ts
  64. 9
      client/contexts/ServerContext/endpoints/v1/teams/addRooms.ts
  65. 5
      client/contexts/ServerContext/endpoints/v1/users/autocomplete.ts
  66. 3
      client/contexts/ServerContext/index.ts
  67. 140
      client/contexts/ServerContext/methods.ts
  68. 3
      client/contexts/ServerContext/methods/followMessage.ts
  69. 3
      client/contexts/ServerContext/methods/roomNameExists.ts
  70. 32
      client/contexts/ServerContext/methods/saveRoomSettings.ts
  71. 9
      client/contexts/ServerContext/methods/saveSettings.ts
  72. 37
      client/contexts/ServerContext/methods/saveUserPreferences.ts
  73. 3
      client/contexts/ServerContext/methods/unfollowMessage.ts
  74. 8
      client/contexts/TranslationContext.ts
  75. 18
      client/hooks/useEndpointData.ts
  76. 4
      client/hooks/useMethodData.ts
  77. 3
      client/hooks/usePolledMethodData.ts
  78. 4
      client/hooks/useRoomIcon.tsx
  79. 1
      client/main.js
  80. 1
      client/polyfills/index.js
  81. 4
      client/providers/SettingsProvider.tsx
  82. 19
      client/sidebar/header/CreateChannel.js
  83. 6
      client/sidebar/header/actions/CreateRoomList.js
  84. 6
      client/sidebar/hooks/useRoomList.ts
  85. 57
      client/types/fuselage.d.ts
  86. 1
      client/types/kadira-flow-router.d.ts
  87. 67
      client/views/InfoPanel/InfoPanel.stories.js
  88. 62
      client/views/InfoPanel/InfoPanel.tsx
  89. 22
      client/views/InfoPanel/RetentionPolicyCallout.tsx
  90. 6
      client/views/InfoPanel/index.ts
  91. 2
      client/views/admin/cloud/PasteStep.tsx
  92. 8
      client/views/admin/customEmoji/EditCustomEmoji.tsx
  93. 7
      client/views/admin/customEmoji/EditCustomEmojiWithData.tsx
  94. 6
      client/views/admin/customEmoji/types.ts
  95. 4
      client/views/admin/customUserStatus/EditCustomUserStatusWithData.tsx
  96. 10
      client/views/admin/rooms/RoomsTable.js
  97. 10
      client/views/admin/sidebar/AdminSidebarSettings.tsx
  98. 4
      client/views/admin/users/UserInfoActions.js
  99. 3
      client/views/directory/DirectoryPage.js
  100. 109
      client/views/directory/TeamsTab.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -40,5 +40,6 @@ import './v1/custom-user-status';
import './v1/instances';
import './v1/banners';
import './v1/email-inbox';
import './v1/teams';
export { API, APIClass, defaultRateLimiterOptions } from './api';

@ -1,5 +1,6 @@
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { Rooms } from '../../../models/server/raw';
import { Subscriptions } from '../../../models';
export async function findAdminRooms({ uid, filter, types = [], pagination: { offset, count, sort } }) {
if (!await hasPermissionAsync(uid, 'view-room-administration')) {
@ -24,11 +25,15 @@ export async function findAdminRooms({ uid, filter, types = [], pagination: { of
msgs: 1,
archived: 1,
tokenpass: 1,
teamId: 1,
teamMain: 1,
};
const name = filter && filter.trim();
const discussion = types && types.includes('discussions');
const showTypes = Array.isArray(types) ? types.filter((type) => type !== 'discussions') : [];
const includeTeams = types && types.includes('teams');
const typesToRemove = ['discussions', 'teams'];
const showTypes = Array.isArray(types) ? types.filter((type) => !typesToRemove.includes(type)) : [];
const options = {
fields,
sort: sort || { default: -1, name: 1 },
@ -36,12 +41,14 @@ export async function findAdminRooms({ uid, filter, types = [], pagination: { of
limit: count,
};
let cursor = Rooms.findByNameContaining(name, discussion, options);
let cursor;
if (name && showTypes.length) {
cursor = Rooms.findByNameContainingAndTypes(name, showTypes, discussion, options);
cursor = Rooms.findByNameContainingAndTypes(name, showTypes, discussion, includeTeams, options);
} else if (showTypes.length) {
cursor = Rooms.findByTypes(showTypes, discussion, options);
cursor = Rooms.findByTypes(showTypes, discussion, includeTeams, options);
} else {
cursor = Rooms.findByNameContaining(name, discussion, includeTeams, options);
}
const total = await cursor.count();
@ -93,6 +100,7 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }) {
const options = {
fields: {
_id: 1,
fname: 1,
name: 1,
t: 1,
avatarETag: 1,
@ -102,8 +110,11 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }) {
name: 1,
},
};
const userRooms = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } })
.fetch()
.map((item) => item.rid);
const rooms = await Rooms.findChannelAndPrivateByNameStarting(selector.name, options).toArray();
const rooms = await Rooms.findChannelAndPrivateByNameStarting(selector.name, userRooms, options).toArray();
return {
items: rooms,

@ -2,11 +2,12 @@ import { Meteor } from 'meteor/meteor';
import _ from 'underscore';
import { Rooms, Subscriptions, Messages, Uploads, Integrations, Users } from '../../../models';
import { hasPermission, hasAtLeastOnePermission } from '../../../authorization/server';
import { hasPermission, hasAtLeastOnePermission, hasAllPermission } from '../../../authorization/server';
import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { API } from '../api';
import { settings } from '../../../settings';
import { Team } from '../../../../server/sdk';
// Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property
@ -184,6 +185,10 @@ function createChannelValidator(params) {
if (params.customFields && params.customFields.value && !(typeof params.customFields.value === 'object')) {
throw new Error(`Param "${ params.customFields.key }" must be an object if provided`);
}
if (params.teams.value && !Array.isArray(params.teams.value)) {
throw new Error(`Param ${ params.teams.key } must be an array`);
}
}
function createChannel(userId, params) {
@ -220,6 +225,10 @@ API.v1.addRoute('channels.create', { authRequired: true }, {
value: bodyParams.members,
key: 'members',
},
teams: {
value: bodyParams.teams,
key: 'teams',
},
});
} catch (e) {
if (e.message === 'unauthorized') {
@ -233,6 +242,21 @@ API.v1.addRoute('channels.create', { authRequired: true }, {
return error;
}
if (bodyParams.teams) {
const canSeeAllTeams = hasPermission(this.userId, 'view-all-teams');
const teams = Promise.await(Team.listByNames(bodyParams.teams, { projection: { _id: 1 } }));
const teamMembers = [];
for (const team of teams) {
const { records: members } = Promise.await(Team.members(this.userId, team._id, undefined, canSeeAllTeams, { offset: 0, count: Number.MAX_SAFE_INTEGER }));
const uids = members.map((member) => member.user.username);
teamMembers.push(...uids);
}
const membersToAdd = new Set([...teamMembers, ...bodyParams.members]);
bodyParams.members = [...membersToAdd];
}
return API.v1.success(API.channels.create.execute(userId, bodyParams));
},
});
@ -472,6 +496,24 @@ API.v1.addRoute('channels.list', { authRequired: true }, {
ourQuery._id = { $in: roomIds };
}
// teams filter - I would love to have a way to apply this filter @ db level :(
const ids = Subscriptions.cachedFindByUserId(this.userId, { fields: { rid: 1 } })
.fetch()
.map((item) => item.rid);
ourQuery.$or = [{
teamId: {
$exists: false,
},
}, {
teamId: {
$exists: true,
},
_id: {
$in: ids,
},
}];
const cursor = Rooms.find(ourQuery, {
sort: sort || { name: 1 },
skip: offset,
@ -1050,3 +1092,51 @@ API.v1.addRoute('channels.anonymousread', { authRequired: false }, {
});
},
});
API.v1.addRoute('channels.convertToTeam', { authRequired: true }, {
post() {
if (!hasAllPermission(this.userId, ['create-team', 'edit-room'])) {
return API.v1.unauthorized();
}
const { channelId, channelName } = this.bodyParams;
if (!channelId && !channelName) {
return API.v1.failure('The parameter "channelId" or "channelName" is required');
}
const room = findChannelByIdOrName({
params: {
roomId: channelId,
roomName: channelName,
},
userId: this.userId,
});
if (!room) {
return API.v1.failure('Channel not found');
}
const subscriptions = Subscriptions.findByRoomId(room._id, {
fields: { 'u._id': 1 },
});
const members = subscriptions.fetch().map((s) => s.u && s.u._id);
const teamData = {
team: {
name: room.name,
type: room.t === 'c' ? 0 : 1,
},
members,
room: {
name: room.name,
id: room._id,
},
};
const team = Promise.await(Team.create(this.userId, teamData));
return API.v1.success({ team });
},
});

@ -4,9 +4,10 @@ import { Match } from 'meteor/check';
import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission';
import { Subscriptions, Rooms, Messages, Uploads, Integrations, Users } from '../../../models/server';
import { hasPermission, hasAtLeastOnePermission, canAccessRoom } from '../../../authorization/server';
import { hasPermission, hasAtLeastOnePermission, canAccessRoom, hasAllPermission } from '../../../authorization/server';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { API } from '../api';
import { Team } from '../../../../server/sdk';
// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property
export function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) {
@ -840,3 +841,51 @@ API.v1.addRoute('groups.setEncrypted', { authRequired: true }, {
});
},
});
API.v1.addRoute('groups.convertToTeam', { authRequired: true }, {
post() {
if (!hasAllPermission(this.userId, ['create-team', 'edit-room'])) {
return API.v1.unauthorized();
}
const { roomId, roomName } = this.requestParams();
if (!roomId && !roomName) {
return API.v1.failure('The parameter "roomId" or "roomName" is required');
}
const room = findPrivateGroupByIdOrName({
params: {
roomId,
roomName,
},
userId: this.userId,
});
if (!room) {
return API.v1.failure('Private group not found');
}
const subscriptions = Subscriptions.findByRoomId(room.rid, {
fields: { 'u._id': 1 },
});
const members = subscriptions.fetch().map((s) => s.u && s.u._id);
const teamData = {
team: {
name: room.name,
type: 1,
},
members,
room: {
name: room.name,
id: room.rid,
},
};
const team = Promise.await(Team.create(this.userId, teamData));
return API.v1.success({ team });
},
});

@ -0,0 +1,321 @@
import { Promise } from 'meteor/promise';
import { API } from '../api';
import { Team } from '../../../../server/sdk';
import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/server';
import { Rooms, Subscriptions } from '../../../models/server';
API.v1.addRoute('teams.list', { authRequired: true }, {
get() {
const { offset, count } = this.getPaginationItems();
const { sort, query } = this.parseJsonQuery();
const { records, total } = Promise.await(Team.list(this.userId, { offset, count }, { sort, query }));
return API.v1.success({
teams: records,
total,
count: records.length,
offset,
});
},
});
API.v1.addRoute('teams.listAll', { authRequired: true }, {
get() {
if (!hasPermission(this.userId, 'view-all-teams')) {
return API.v1.unauthorized();
}
const { offset, count } = this.getPaginationItems();
const { records, total } = Promise.await(Team.listAll({ offset, count }));
return API.v1.success({
teams: records,
total,
count: records.length,
offset,
});
},
});
API.v1.addRoute('teams.create', { authRequired: true }, {
post() {
if (!hasPermission(this.userId, 'create-team')) {
return API.v1.unauthorized();
}
const { name, type, members, room, owner } = this.bodyParams;
if (!name) {
return API.v1.failure('Body param "name" is required');
}
const team = Promise.await(Team.create(this.userId, {
team: {
name,
type,
},
room,
members,
owner,
}));
return API.v1.success({ team });
},
});
API.v1.addRoute('teams.addRoom', { authRequired: true }, {
post() {
const { roomId, teamId, isDefault } = this.bodyParams;
if (!hasPermission(this.userId, 'add-team-channel')) {
return API.v1.unauthorized();
}
const room = Promise.await(Team.addRoom(this.userId, roomId, teamId, isDefault));
return API.v1.success({ room });
},
});
API.v1.addRoute('teams.addRooms', { authRequired: true }, {
post() {
const { rooms, teamId } = this.bodyParams;
if (!hasPermission(this.userId, 'add-team-channel')) {
return API.v1.unauthorized();
}
const validRooms = Promise.await(Team.addRooms(this.userId, rooms, teamId));
return API.v1.success({ rooms: validRooms });
},
});
API.v1.addRoute('teams.removeRoom', { authRequired: true }, {
post() {
const { roomId, teamId } = this.bodyParams;
if (!hasPermission(this.userId, 'remove-team-channel')) {
return API.v1.unauthorized();
}
const canRemoveAny = !!hasPermission(this.userId, 'view-all-team-channels');
const room = Promise.await(Team.removeRoom(this.userId, roomId, teamId, canRemoveAny));
return API.v1.success({ room });
},
});
API.v1.addRoute('teams.updateRoom', { authRequired: true }, {
post() {
const { roomId, isDefault } = this.bodyParams;
if (!hasPermission(this.userId, 'edit-team-channel')) {
return API.v1.unauthorized();
}
const canUpdateAny = !!hasPermission(this.userId, 'view-all-team-channels');
const room = Promise.await(Team.updateRoom(this.userId, roomId, isDefault, canUpdateAny));
return API.v1.success({ room });
},
});
API.v1.addRoute('teams.listRooms', { authRequired: true }, {
get() {
const { teamId } = this.queryParams;
const { offset, count } = this.getPaginationItems();
const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams');
let getAllRooms = false;
if (hasPermission(this.userId, 'view-all-team-channels')) {
getAllRooms = true;
}
const { records, total } = Promise.await(Team.listRooms(this.userId, teamId, getAllRooms, allowPrivateTeam, { offset, count }));
return API.v1.success({
rooms: records,
total,
count: records.length,
offset,
});
},
});
API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, {
get() {
const { offset, count } = this.getPaginationItems();
const { teamId, userId } = this.queryParams;
const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams');
if (!hasPermission(this.userId, 'view-all-team-channels')) {
return API.v1.unauthorized();
}
const { records, total } = Promise.await(Team.listRoomsOfUser(this.userId, teamId, userId, allowPrivateTeam, { offset, count }));
return API.v1.success({
rooms: records,
total,
count: records.length,
offset: 0,
});
},
});
API.v1.addRoute('teams.members', { authRequired: true }, {
get() {
const { offset, count } = this.getPaginationItems();
const { teamId, teamName } = this.queryParams;
const { query } = this.parseJsonQuery();
const canSeeAllMembers = hasPermission(this.userId, 'view-all-teams');
const { records, total } = Promise.await(Team.members(this.userId, teamId, teamName, canSeeAllMembers, { offset, count }, { query }));
return API.v1.success({
members: records,
total,
count: records.length,
offset,
});
},
});
API.v1.addRoute('teams.addMembers', { authRequired: true }, {
post() {
if (!hasAtLeastOnePermission(this.userId, ['add-team-member', 'edit-team-member'])) {
return API.v1.unauthorized();
}
const { teamId, teamName, members } = this.bodyParams;
Promise.await(Team.addMembers(this.userId, teamId, teamName, members));
return API.v1.success();
},
});
API.v1.addRoute('teams.updateMember', { authRequired: true }, {
post() {
if (!hasAtLeastOnePermission(this.userId, ['edit-team-member'])) {
return API.v1.unauthorized();
}
const { teamId, teamName, member } = this.bodyParams;
Promise.await(Team.updateMember(teamId, teamName, member));
return API.v1.success();
},
});
API.v1.addRoute('teams.removeMembers', { authRequired: true }, {
post() {
if (!hasAtLeastOnePermission(this.userId, ['edit-team-member'])) {
return API.v1.unauthorized();
}
const { teamId, teamName, members, rooms } = this.bodyParams;
Promise.await(Team.removeMembers(teamId, teamName, members));
if (rooms?.length) {
Subscriptions.removeByRoomIdsAndUserId(rooms, this.userId);
}
return API.v1.success();
},
});
API.v1.addRoute('teams.leave', { authRequired: true }, {
post() {
const { teamId, teamName, rooms } = this.bodyParams;
Promise.await(Team.removeMembers(teamId, teamName, [{
userId: this.userId,
}]));
if (rooms?.length) {
Subscriptions.removeByRoomIdsAndUserId(rooms, this.userId);
}
return API.v1.success();
},
});
API.v1.addRoute('teams.info', { authRequired: true }, {
get() {
const { teamId, teamName } = this.queryParams;
if (!teamId && !teamName) {
return API.v1.failure('Provide either the "teamId" or "teamName"');
}
const teamInfo = teamId
? Promise.await(Team.getInfoById(teamId))
: Promise.await(Team.getInfoByName(teamName));
if (!teamInfo) {
return API.v1.failure('Team not found');
}
return API.v1.success({ teamInfo });
},
});
API.v1.addRoute('teams.delete', { authRequired: true }, {
post() {
if (!hasPermission(this.userId, 'delete-team')) {
return API.v1.unauthorized();
}
const { teamId, teamName, roomsToRemove } = this.bodyParams;
if (!teamId && !teamName) {
return API.v1.failure('Provide either the "teamId" or "teamName"');
}
if (roomsToRemove && !Array.isArray(roomsToRemove)) {
return API.v1.failure('The list of rooms to remove is invalid.');
}
const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName));
if (!team) {
return API.v1.failure('Team not found.');
}
const rooms = Promise.await(Team.getMatchingTeamRooms(team._id, roomsToRemove));
// Remove the team's main room
Rooms.removeById(team.roomId);
// If we got a list of rooms to delete along with the team, remove them first
if (rooms.length) {
Rooms.removeByIds(rooms);
}
// Move every other room back to the workspace
Promise.await(Team.unsetTeamIdOfRooms(team._id));
// And finally delete the team itself
Promise.await(Team.deleteById(team._id));
return API.v1.success();
},
});
API.v1.addRoute('teams.autocomplete', { authRequired: true }, {
get() {
const { name, userId } = this.queryParams;
const teams = Promise.await(Team.autocomplete(userId, name));
return API.v1.success({ teams });
},
});

@ -25,6 +25,7 @@ import { getUserForCheck, emailCheck } from '../../../2fa/server/code';
import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';
import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers';
import { resetTOTP } from '../../../2fa/server/functions/resetTOTP';
import { Team } from '../../../../server/sdk';
API.v1.addRoute('users.create', { authRequired: true }, {
post() {
@ -909,3 +910,18 @@ API.v1.addRoute('users.resetTOTP', { authRequired: true, twoFactorRequired: true
return API.v1.success();
},
});
API.v1.addRoute('users.listTeams', { authRequired: true }, {
get() {
const { userId } = this.bodyParams;
// If the caller has permission to view all teams, there's no need to filter the teams
const adminId = hasPermission(this.userId, 'view-all-teams') ? '' : this.userId;
const teams = Promise.await(Team.findBySubscribedUserIds(userId, adminId));
return API.v1.success({
teams,
});
},
});

@ -18,7 +18,7 @@ addAction('game-center', () => {
&& !hasError
&& hasExternalComponents
? {
groups: ['channel', 'group', 'direct'],
groups: ['channel', 'group', 'direct', 'team'],
id: 'game-center',
title: 'Apps_Game_Center',
icon: 'game',

@ -124,6 +124,16 @@ Meteor.startup(function() {
{ _id: 'send-omnichannel-chat-transcript', roles: ['livechat-manager', 'admin'] },
{ _id: 'mail-messages', roles: ['admin'] },
{ _id: 'toggle-room-e2e-encryption', roles: ['owner'] },
{ _id: 'create-team', roles: ['admin', 'user'] },
{ _id: 'delete-team', roles: ['admin', 'team-owner'] },
{ _id: 'edit-team', roles: ['admin', 'team-owner'] },
{ _id: 'add-team-member', roles: ['admin', 'team-owner', 'team-moderator'] },
{ _id: 'edit-team-member', roles: ['admin', 'team-owner', 'team-moderator'] },
{ _id: 'add-team-channel', roles: ['admin', 'team-owner', 'team-moderator'] },
{ _id: 'edit-team-channel', roles: ['admin', 'team-owner', 'team-moderator'] },
{ _id: 'remove-team-channel', roles: ['admin', 'team-owner', 'team-moderator'] },
{ _id: 'view-all-team-channels', roles: ['admin', 'team-owner'] },
{ _id: 'view-all-teams', roles: ['admin'] },
];
for (const permission of permissions) {
@ -135,6 +145,9 @@ Meteor.startup(function() {
{ name: 'moderator', scope: 'Subscriptions', description: 'Moderator' },
{ name: 'leader', scope: 'Subscriptions', description: 'Leader' },
{ name: 'owner', scope: 'Subscriptions', description: 'Owner' },
{ name: 'team-owner', scope: 'Subscriptions', description: 'Team Owner' },
{ name: 'team-moderator', scope: 'Subscriptions', description: 'Team Moderator' },
{ name: 'team-leader', scope: 'Subscriptions', description: 'Team Leader' },
{ name: 'user', scope: 'Users', description: '' },
{ name: 'bot', scope: 'Users', description: '' },
{ name: 'app', scope: 'Users', description: '' },

@ -8,7 +8,7 @@ addAction('autotranslate', () => {
const hasPermission = usePermission('auto-translate');
const autoTranslateEnabled = useSetting('AutoTranslate_Enabled');
return useMemo(() => (hasPermission && autoTranslateEnabled ? {
groups: ['channel', 'group', 'direct'],
groups: ['channel', 'group', 'direct', 'team'],
id: 'autotranslate',
title: 'Auto_Translate',
icon: 'language',

@ -9,7 +9,7 @@ addAction('discussions', () => {
const discussionEnabled = useSetting('Discussion_enabled');
return useMemo(() => (discussionEnabled ? {
groups: ['channel', 'group', 'direct'],
groups: ['channel', 'group', 'direct', 'team'],
id: 'discussions',
title: 'Discussions',
icon: 'discussion',

@ -21,7 +21,7 @@ addAction('e2e', ({ room }) => {
const enabledOnRoom = !!room.encrypted;
return useMemo(() => (e2eEnabled && hasPermission ? {
groups: ['direct', 'group'],
groups: ['direct', 'group', 'team'],
id: 'e2e',
title: enabledOnRoom ? 'E2E_disable' : 'E2E_enable',
icon: 'key',

@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor';
import { AppEvents, Apps } from '../../../apps/server';
import { callbacks } from '../../../callbacks';
import { Messages, Rooms, Subscriptions } from '../../../models';
import { Team } from '../../../../server/sdk';
import { RoomMemberActions, roomTypes } from '../../../utils/server';
export const addUserToRoom = function(rid, user, inviter, silenced) {
@ -84,5 +85,10 @@ export const addUserToRoom = function(rid, user, inviter, silenced) {
});
}
if (room.teamMain && room.teamId) {
// if user is joining to main team channel, create a membership
Promise.await(Team.addMember(inviter, user._id, room.teamId));
}
return true;
};

@ -9,9 +9,10 @@ import { callbacks } from '../../../callbacks';
import { Rooms, Subscriptions, Users } from '../../../models';
import { getValidRoomName } from '../../../utils';
import { createDirectRoom } from './createDirectRoom';
import { Team } from '../../../../server/sdk';
export const createRoom = function(type, name, owner, members = [], readOnly, extraData = {}, options = {}) {
export const createRoom = function(type, name, owner, members = [], readOnly, { teamId, ...extraData } = {}, options = {}) {
callbacks.run('beforeCreateRoom', { type, name, owner, members, readOnly, extraData, options });
if (type === 'd') {
@ -64,6 +65,13 @@ export const createRoom = function(type, name, owner, members = [], readOnly, ex
ro: readOnly === true,
};
if (teamId) {
const team = Promise.await(Team.getOneById(teamId, { projection: { _id: 1 } }));
if (team) {
room.teamId = team._id;
}
}
room._USERNAMES = members;
const prevent = Promise.await(Apps.triggerEvent('IPreRoomCreatePrevent', room).catch((error) => {

@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor';
import { Rooms, Messages, Subscriptions } from '../../../models';
import { AppEvents, Apps } from '../../../apps/server';
import { callbacks } from '../../../callbacks';
import { Team } from '../../../../server/sdk';
export const removeUserFromRoom = function(rid, user, options = {}) {
const room = Rooms.findOneById(rid);
@ -40,6 +41,10 @@ export const removeUserFromRoom = function(rid, user, options = {}) {
Subscriptions.removeByRoomIdAndUserId(rid, user._id);
if (room.teamId && room.teamMain) {
Promise.await(Team.removeMember(room.teamId, user._id));
}
Meteor.defer(function() {
// TODO: CACHE: maybe a queue?
callbacks.run('afterLeaveRoom', user, room);

@ -13,7 +13,7 @@ addAction('livestream', ({ room }) => {
const isLive = room && room.streamingOptions && room.streamingOptions.id && room.streamingOptions.type === 'livestream';
return useMemo(() => (enabled ? {
groups: ['channel', 'group'],
groups: ['channel', 'group', 'team'],
id: 'livestream',
title: 'Livestream',
icon: 'podcast',

@ -1,7 +1,7 @@
import { addAction } from '../../../client/views/room/lib/Toolbox';
addAction('mentions', {
groups: ['channel', 'group'],
groups: ['channel', 'group', 'team'],
id: 'mentions',
title: 'Mentions',
icon: 'at',

@ -6,7 +6,7 @@ import { useSetting } from '../../../client/contexts/SettingsContext';
addAction('pinned-messages', () => {
const pinningAllowed = useSetting('Message_AllowPinning');
return useMemo(() => (pinningAllowed ? {
groups: ['channel', 'group', 'direct'],
groups: ['channel', 'group', 'direct', 'team'],
id: 'pinned-messages',
title: 'Pinned_Messages',
icon: 'pin',

@ -6,7 +6,7 @@ import { useSetting } from '../../../../client/contexts/SettingsContext';
addAction('snippeted-messages', () => {
const snippetingEnabled = useSetting('Message_AllowSnippeting');
return useMemo(() => (snippetingEnabled ? {
groups: ['channel', 'group', 'direct'],
groups: ['channel', 'group', 'direct', 'team'],
id: 'snippeted-messages',
title: 'snippet-message',
icon: 'code',

@ -1,7 +1,7 @@
import { addAction } from '../../../client/views/room/lib/Toolbox';
addAction('starred-messages', {
groups: ['channel', 'group', 'direct', 'live'],
groups: ['channel', 'group', 'direct', 'live', 'team'],
id: 'starred-messages',
title: 'Starred_Messages',
icon: 'star',

@ -40,7 +40,6 @@ export class Rooms extends Base {
return this.findOne(query, options);
}
setJitsiTimeout(_id, time) {
const query = {
_id,
@ -133,7 +132,7 @@ export class Rooms extends Base {
}
setLastMessageSnippeted(roomId, message, snippetName, snippetedBy, snippeted, snippetedAt) {
const query = { _id: roomId };
const query = { _id: roomId };
const msg = `\`\`\`${ message.msg }\`\`\``;
@ -261,7 +260,6 @@ export class Rooms extends Base {
return this.update({ _id }, update);
}
setSystemMessagesById = function(_id, systemMessages) {
const query = {
_id,
@ -275,6 +273,7 @@ export class Rooms extends Base {
sysMes: '',
},
};
return this.update(query, update);
}
@ -339,6 +338,9 @@ export class Rooms extends Base {
const query = {
name,
t: type,
teamId: {
$exists: false,
},
};
return this.findOne(query, options);
@ -388,20 +390,36 @@ export class Rooms extends Base {
}
findBySubscriptionUserId(userId, options) {
const data = Subscriptions.findByUserId(userId, { fields: { rid: 1 } }).fetch()
const data = Subscriptions.cachedFindByUserId(userId, { fields: { rid: 1 } })
.fetch()
.map((item) => item.rid);
const query = {
_id: {
$in: data,
},
$or: [{
teamId: {
$exists: false,
},
}, {
teamId: {
$exists: true,
},
_id: {
$in: data,
},
}],
};
return this.find(query, options);
}
findBySubscriptionTypeAndUserId(type, userId, options) {
const data = Subscriptions.findByUserIdAndType(userId, type, { fields: { rid: 1 } }).fetch()
const data = Subscriptions.findByUserIdAndType(userId, type, {
fields: { rid: 1 },
})
.fetch()
.map((item) => item.rid);
const query = {
@ -415,7 +433,8 @@ export class Rooms extends Base {
}
findBySubscriptionUserIdUpdatedAfter(userId, _updatedAt, options) {
const ids = Subscriptions.findByUserId(userId, { fields: { rid: 1 } }).fetch()
const ids = Subscriptions.findByUserId(userId, { fields: { rid: 1 } })
.fetch()
.map((item) => item.rid);
const query = {
@ -425,6 +444,18 @@ export class Rooms extends Base {
_updatedAt: {
$gt: _updatedAt,
},
$or: [{
teamId: {
$exists: false,
},
}, {
teamId: {
$exists: true,
},
_id: {
$in: ids,
},
}],
};
return this.find(query, options);
@ -478,6 +509,9 @@ export class Rooms extends Base {
findByNameOrFNameAndType(name, type, options) {
const query = {
t: type,
teamId: {
$exists: false,
},
$or: [{
name,
}, {
@ -496,6 +530,9 @@ export class Rooms extends Base {
default: {
$ne: true,
},
teamId: {
$exists: false,
},
};
// do not use cache
@ -510,6 +547,21 @@ export class Rooms extends Base {
t: {
$in: types,
},
$or: [
{
teamId: {
$exists: false,
},
},
{
teamId: {
$exists: true,
},
_id: {
$in: ids,
},
},
],
name,
};
@ -517,7 +569,7 @@ export class Rooms extends Base {
return this._db.find(query, options);
}
findChannelAndPrivateByNameStarting(name, options) {
findChannelAndPrivateByNameStarting(name, sIds, options) {
const nameRegex = new RegExp(`^${ s.trim(escapeRegExp(name)) }`, 'i');
const query = {
@ -525,6 +577,21 @@ export class Rooms extends Base {
$in: ['c', 'p'],
},
name: nameRegex,
teamMain: {
$exists: false,
},
$or: [{
teamId: {
$exists: false,
},
}, {
teamId: {
$exists: true,
},
_id: {
$in: sIds,
},
}],
};
return this.find(query, options);
@ -572,10 +639,7 @@ export class Rooms extends Base {
findByTypeAndNameOrId(type, identifier, options) {
const query = {
t: type,
$or: [
{ name: identifier },
{ _id: identifier },
],
$or: [{ name: identifier }, { _id: identifier }],
};
return this.findOne(query, options);
@ -710,7 +774,9 @@ export class Rooms extends Base {
}
incMsgCountAndSetLastMessageById(_id, inc, lastMessageTimestamp, lastMessage) {
if (inc == null) { inc = 1; }
if (inc == null) {
inc = 1;
}
const query = { _id };
const update = {
@ -745,6 +811,22 @@ export class Rooms extends Base {
return this.update(query, update);
}
incUsersCountByIds(ids, inc = 1) {
const query = {
_id: {
$in: ids,
},
};
const update = {
$inc: {
usersCount: inc,
},
};
return this.update(query, update, { multi: true });
}
incUsersCountNotDMsByIds(ids, inc = 1) {
const query = {
_id: {
@ -978,7 +1060,7 @@ export class Rooms extends Base {
const update = {
...favorite && defaultValue && { $set: { favorite } },
...(!favorite || !defaultValue) && { $unset: { favorite: 1 } },
...(!favorite || !defaultValue) && { $unset: { favorite: 1 } },
};
return this.update(query, update);

@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import _ from 'underscore';
import mem from 'mem';
import { Base } from './_Base';
import Rooms from './Rooms';
@ -422,11 +423,13 @@ export class Subscriptions extends Base {
// FIND
findByUserId(userId, options) {
const query = { 'u._id': userId };
const query = { 'u._id': userId };
return this.find(query, options);
}
cachedFindByUserId = mem(this.findByUserId.bind(this), { maxAge: 5000 });
findByUserIdExceptType(userId, typeException, options) {
const query = {
'u._id': userId,
@ -436,6 +439,19 @@ export class Subscriptions extends Base {
return this.find(query, options);
}
findByUserIdWithRoomInfo(userId, options) {
const userSubs = this.find({ 'u._id': userId }, options);
const roomIds = userSubs.map((sub) => sub.rid);
const rooms = Rooms.findByIds(roomIds, { projection: { _id: 1, teamId: 1, teamMain: 1 } }).fetch();
return userSubs.map((sub) => {
const roomSub = rooms.find((r) => r._id === sub.rid);
sub.teamMain = roomSub?.teamMain || false;
sub.teamId = roomSub?.teamId || undefined;
return sub;
});
}
findByUserIdAndType(userId, type, options) {
const query = {
'u._id': userId,
@ -592,7 +608,7 @@ export class Subscriptions extends Base {
// UPDATE
archiveByRoomId(roomId) {
const query = { rid: roomId };
const query = { rid: roomId };
const update = {
$set: {
@ -1258,6 +1274,18 @@ export class Subscriptions extends Base {
return result;
}
removeByRoomIdsAndUserId(rids, userId) {
const result = this.remove({ rid: { $in: rids }, 'u._id': userId });
if (Match.test(result, Number) && result > 0) {
Rooms.incUsersCountByIds(rids, -1);
}
Users.removeRoomsByRoomIdsAndUserId(rids, userId);
return result;
}
// //////////////////////////////////////////////////////////////////
// threads

@ -443,6 +443,15 @@ export class Users extends Base {
}, { multi: true });
}
removeRoomsByRoomIdsAndUserId(rids, userId) {
return this.update({
_id: userId,
__rooms: { $in: rids },
}, {
$pullAll: { __rooms: rids },
}, { multi: true });
}
update2FABackupCodesByUserId(userId, backupCodes) {
return this.update({
_id: userId,

@ -6,6 +6,7 @@ import {
FilterQuery,
FindOneOptions,
InsertOneWriteOpResult,
InsertWriteOpResult,
ObjectID,
ObjectId,
OptionalId,
@ -113,6 +114,19 @@ export class BaseRaw<T> implements IBaseRaw<T> {
return this.col.updateMany(filter, update, options);
}
insertMany(docs: Array<ModelOptionalId<T>>, options?: CollectionInsertOneOptions): Promise<InsertWriteOpResult<WithId<T>>> {
docs = docs.map((doc) => {
if (!doc._id || typeof doc._id !== 'string') {
const oid = new ObjectID();
return { _id: oid.toHexString(), ...doc };
}
return doc;
});
// TODO reavaluate following type casting
return this.col.insertMany(docs as unknown as Array<OptionalId<T>>, options);
}
insertOne(doc: ModelOptionalId<T>, options?: CollectionInsertOneOptions): Promise<InsertOneWriteOpResult<WithId<T>>> {
if (!doc._id || typeof doc._id !== 'string') {
const oid = new ObjectID();

@ -2,15 +2,25 @@ import { escapeRegExp } from '../../../../lib/escapeRegExp';
import { BaseRaw } from './BaseRaw';
export class RoomsRaw extends BaseRaw {
findOneByRoomIdAndUserId(rid, uid, options) {
findOneByRoomIdAndUserId(rid, uid, options = {}) {
const query = {
rid,
_id: rid,
'u._id': uid,
};
return this.findOne(query, options);
}
findManyByRoomIds(roomIds, options = {}) {
const query = {
_id: {
$in: roomIds,
},
};
return this.find(query, options);
}
async getMostRecentAverageChatDurationTime(numberMostRecentChats, department) {
const aggregate = [
{
@ -32,8 +42,15 @@ export class RoomsRaw extends BaseRaw {
return statistic;
}
findByNameContainingAndTypes(name, types, discussion = false, options = {}) {
findByNameContainingAndTypes(name, types, discussion = false, teams = false, options = {}) {
const nameRegex = new RegExp(escapeRegExp(name).trim(), 'i');
const teamCondition = teams ? {} : {
teamMain: {
$exists: false,
},
};
const query = {
t: {
$in: types,
@ -46,23 +63,37 @@ export class RoomsRaw extends BaseRaw {
usernames: nameRegex,
},
],
...teamCondition,
};
return this.find(query, options);
}
findByTypes(types, discussion = false, options = {}) {
findByTypes(types, discussion = false, teams = false, options = {}) {
const teamCondition = teams ? {} : {
teamMain: {
$exists: false,
},
};
const query = {
t: {
$in: types,
},
prid: { $exists: discussion },
...teamCondition,
};
return this.find(query, options);
}
findByNameContaining(name, discussion = false, options = {}) {
findByNameContaining(name, discussion = false, teams = false, options = {}) {
const nameRegex = new RegExp(escapeRegExp(name).trim(), 'i');
const teamCondition = teams ? {} : {
teamMain: {
$exists: false,
},
};
const query = {
prid: { $exists: discussion },
$or: [
@ -72,11 +103,44 @@ export class RoomsRaw extends BaseRaw {
usernames: nameRegex,
},
],
...teamCondition,
};
return this.find(query, options);
}
findByTeamId(teamId, options = {}) {
const query = {
teamId,
teamMain: {
$exists: false,
},
};
return this.find(query, options);
}
findByTeamIdAndRoomsId(teamId, rids, options = {}) {
const query = {
teamId,
_id: {
$in: rids,
},
};
return this.find(query, options);
}
findChannelAndPrivateByNameStarting(name, options) {
findPublicByTeamId(uid, teamId, options = {}) {
const query = {
teamId,
t: 'c',
};
return this.find(query, options);
}
findChannelAndPrivateByNameStarting(name, sIds, options) {
const nameRegex = new RegExp(`^${ escapeRegExp(name).trim() }`, 'i');
const query = {
@ -84,11 +148,58 @@ export class RoomsRaw extends BaseRaw {
$in: ['c', 'p'],
},
name: nameRegex,
teamMain: {
$exists: false,
},
$or: [{
teamId: {
$exists: false,
},
}, {
teamId: {
$exists: true,
},
_id: {
$in: sIds,
},
}],
};
return this.find(query, options);
}
unsetTeamId(teamId, options = {}) {
const query = { teamId };
const update = {
$unset: {
teamId: '',
teamDefault: '',
},
};
return this.update(query, update, options);
}
unsetTeamById(rid, options = {}) {
return this.updateOne({ _id: rid }, { $unset: { teamId: '', teamDefault: '' } }, options);
}
setTeamById(rid, teamId, teamDefault, options = {}) {
return this.updateOne({ _id: rid }, { $set: { teamId, teamDefault } }, options);
}
setTeamMainById(rid, teamId, options = {}) {
return this.updateOne({ _id: rid }, { $set: { teamId, teamMain: true } }, options);
}
setTeamByIds(rids, teamId, options = {}) {
return this.updateMany({ _id: { $in: rids } }, { $set: { teamId } }, options);
}
setTeamDefaultById(rid, teamDefault, options = {}) {
return this.updateOne({ _id: rid }, { $set: { teamDefault } }, options);
}
findChannelsWithNumberOfMessagesBetweenDate({ start, end, startOfLastWeek, endOfLastWeek, onlyCount = false, options = {} }) {
const lookup = {
$lookup: {
@ -191,4 +302,18 @@ export class RoomsRaw extends BaseRaw {
return this.col.aggregate(params).toArray();
}
findOneByName(name, options = {}) {
return this.col.findOne({ name }, options);
}
findDefaultRoomsForTeam(teamId) {
return this.col.find({
teamId,
teamDefault: true,
teamMain: {
$exists: false,
},
});
}
}

@ -14,6 +14,17 @@ export class SubscriptionsRaw extends BaseRaw<T> {
return this.findOne(query, options);
}
findByUserIdAndRoomIds(userId: string, roomIds: Array<string>, options: FindOneOptions<T> = {}): Cursor<T> {
const query = {
'u._id': userId,
rid: {
$in: roomIds,
},
};
return this.find(query, options);
}
findByRoomIdAndNotUserId(roomId: string, userId: string, options: FindOneOptions<T> = {}): Cursor<T> {
const query = {
rid: roomId,

@ -0,0 +1,73 @@
import { Collection, FindOneOptions, Cursor, UpdateWriteOpResult, DeleteWriteOpResultObject, FilterQuery } from 'mongodb';
import { BaseRaw } from './BaseRaw';
import { ITeam } from '../../../../definition/ITeam';
type T = ITeam;
export class TeamRaw extends BaseRaw<T> {
constructor(
public readonly col: Collection<T>,
public readonly trash?: Collection<T>,
) {
super(col, trash);
this.col.createIndex({ name: 1 }, { unique: true });
// this.col.createIndexes([
// { key: { status: 1, expireAt: 1 } },
// ]);
}
findByNames(names: Array<string>, options?: FindOneOptions<T>): Cursor<T> {
return this.col.find({ name: { $in: names } }, options);
}
findByIds(ids: Array<string>, options?: FindOneOptions<T>, query?: FilterQuery<T>): Cursor<T> {
return this.col.find({ _id: { $in: ids }, ...query }, options);
}
findByIdsAndType(ids: Array<string>, type: number, options?: FindOneOptions<T>): Cursor<T> {
return this.col.find({ _id: { $in: ids }, type }, options);
}
findByNameAndTeamIds(name: string | RegExp, teamIds: Array<string>, options?: FindOneOptions<T>): Cursor<T> {
return this.col.find({
name,
$or: [{
type: 0,
}, {
_id: {
$in: teamIds,
},
}],
}, options);
}
findOneByName(name: string, options?: FindOneOptions<T>): Promise<T | null> {
return this.col.findOne({ name }, options);
}
findOneByMainRoomId(roomId: string, options?: FindOneOptions<T>): Promise<T | null> {
return this.col.findOne({ roomId }, options);
}
updateMainRoomForTeam(id: string, roomId: string): Promise<UpdateWriteOpResult> {
return this.col.updateOne({
_id: id,
}, {
$set: {
roomId,
},
});
}
deleteOneById(id: string): Promise<DeleteWriteOpResultObject> {
return this.col.deleteOne({
_id: id,
});
}
deleteOneByName(name: string): Promise<DeleteWriteOpResultObject> {
return this.col.deleteOne({ name });
}
}

@ -0,0 +1,105 @@
import { Collection, FindOneOptions, Cursor, InsertOneWriteOpResult, UpdateWriteOpResult, DeleteWriteOpResultObject, FilterQuery } from 'mongodb';
import { BaseRaw } from './BaseRaw';
import { ITeamMember } from '../../../../definition/ITeam';
import { IUser } from '../../../../definition/IUser';
type T = ITeamMember;
export class TeamMemberRaw extends BaseRaw<T> {
constructor(
public readonly col: Collection<T>,
public readonly trash?: Collection<T>,
) {
super(col, trash);
this.col.createIndexes([
{ key: { teamId: 1 } },
]);
// teamId => userId should be unique
this.col.createIndex({ teamId: 1, userId: 1 }, { unique: true });
}
findByUserId(userId: string, options?: FindOneOptions<T>): Cursor<T> {
return this.col.find({ userId }, options);
}
findOneByUserIdAndTeamId(userId: string, teamId: string, options?: FindOneOptions<T>): Promise<T | null> {
return this.col.findOne({ userId, teamId }, options);
}
findByTeamId(teamId: string, options?: FindOneOptions<T>): Cursor<T> {
return this.col.find({ teamId }, options);
}
findByTeamIdAndRole(teamId: string, role?: string, options?: FindOneOptions<T>): Cursor<T> {
return this.col.find({ teamId, roles: role }, options);
}
findByUserIdAndTeamIds(userId: string, teamIds: Array<string>, options: FindOneOptions<T> = {}): Cursor<T> {
const query = {
'u._id': userId,
teamId: {
$in: teamIds,
},
};
return this.col.find(query, options);
}
findMembersInfoByTeamId(teamId: string, limit: number, skip: number, query?: FilterQuery<T>): Cursor<T> {
return this.col.find({ teamId, ...query }, {
limit,
skip,
projection: {
userId: 1,
roles: 1,
createdBy: 1,
createdAt: 1,
},
} as FindOneOptions<T>);
}
updateOneByUserIdAndTeamId(userId: string, teamId: string, update: Partial<T>): Promise<UpdateWriteOpResult> {
return this.col.updateOne({ userId, teamId }, { $set: update });
}
createOneByTeamIdAndUserId(teamId: string, userId: string, createdBy: Pick<IUser, '_id' | 'username'>): Promise<InsertOneWriteOpResult<T>> {
return this.insertOne({
teamId,
userId,
createdAt: new Date(),
_updatedAt: new Date(),
createdBy,
});
}
updateRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array<string>): Promise<UpdateWriteOpResult> {
return this.col.updateOne({
teamId,
userId,
}, {
$addToSet: {
roles: { $each: roles },
},
});
}
removeRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array<string>): Promise<UpdateWriteOpResult> {
return this.col.updateOne({
teamId,
userId,
}, {
$pull: {
roles: { $in: roles },
},
});
}
deleteByUserIdAndTeamId(userId: string, teamId: string): Promise<DeleteWriteOpResultObject> {
return this.col.deleteOne({
teamId,
userId,
});
}
}

@ -110,6 +110,15 @@ export class UsersRaw extends BaseRaw {
return this.find(query, options);
}
findActiveByIds(userIds, options = {}) {
const query = {
_id: { $in: userIds },
active: true,
};
return this.find(query, options);
}
findOneByUsernameIgnoringCase(username, options) {
if (typeof username === 'string') {
username = new RegExp(`^${ escapeRegExp(username) }$`, 'i');

@ -3,7 +3,7 @@ import { lazy } from 'react';
import { addAction } from '../../../client/views/room/lib/Toolbox';
addAction('push-notifications', {
groups: ['channel', 'group', 'direct', 'live'],
groups: ['channel', 'group', 'direct', 'live', 'team'],
id: 'push-notifications',
title: 'Notifications_Preferences',
icon: 'bell',

@ -24,6 +24,7 @@ import { NotificationQueue, Users as UsersRaw } from '../../../models/server/raw
import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred';
import { getAppsStatistics } from './getAppsStatistics';
import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server';
import { Team } from '../../../../server/sdk';
const wizardFields = [
'Organization_Type',
@ -103,6 +104,9 @@ export const statistics = {
statistics.totalDiscussions = Rooms.countDiscussions();
statistics.totalThreads = Messages.countThreads();
// Teams statistics
statistics.teams = Promise.await(Team.getStatistics());
// livechat visitors
statistics.totalLivechatVisitors = LivechatVisitors.find().count();

@ -22,7 +22,7 @@ const subscriptionFields = {};
const useThreadMessage = (tmid: string): IMessage => {
const [message, setMessage] = useState<IMessage>(() => Tracker.nonreactive(() => ChatMessage.findOne({ _id: tmid })));
const getMessage = useEndpoint('GET', 'chat.getMessage');
const getMessageParsed = useCallback<(params: Mongo.Query<IMessage>) => Promise<IMessage>>(async (params) => {
const getMessageParsed = useCallback<(params: Parameters<typeof getMessage>[0]) => Promise<IMessage>>(async (params) => {
const { message } = await getMessage(params);
return {
...message,

@ -13,10 +13,6 @@ import Notifications from '../../../notifications/client/lib/Notifications';
import { getConfig } from '../../../ui-utils/client/config';
import { callMethod } from '../../../ui-utils/client/lib/callMethod';
const fromEntries = Object.fromEntries || function fromEntries(iterable) {
return [...iterable].reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {});
};
const wrap = (fn) => (...args) => new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) {
@ -129,7 +125,7 @@ export class CachedCollection extends Emitter {
userRelated = true,
listenChangesForLoggedUsersOnly = false,
useSync = true,
version = 15,
version = 16,
maxCacheTime = 60 * 60 * 24 * 30,
onSyncData = (/* action, record */) => {},
}) {
@ -210,7 +206,7 @@ export class CachedCollection extends Emitter {
}
});
this.collection._collection._docs._map = fromEntries(data.records.map((record) => [record._id, record]));
this.collection._collection._docs._map = Object.fromEntries(data.records.map((record) => [record._id, record]));
this.updatedAt = data.updatedAt || this.updatedAt;
Object.values(this.collection._collection.queries).forEach((query) => this.collection._collection._recomputeResults(query));

@ -3,7 +3,7 @@ export { getDefaultSubscriptionPref } from '../lib/getDefaultSubscriptionPref';
export { Info } from '../rocketchat.info';
export { getUserPreference } from '../lib/getUserPreference';
export { fileUploadMediaWhiteList, fileUploadIsValidContentType } from '../lib/fileUploadRestrictions';
export { roomTypes } from './lib/roomTypes';
export { roomTypes, searchableRoomTypes } from './lib/roomTypes';
export { RoomTypeRouteConfig, RoomTypeConfig, RoomSettingsEnum, RoomMemberActions, UiTextContext } from '../lib/RoomTypeConfig';
export { RoomTypesCommon } from '../lib/RoomTypesCommon';
export { isDocker } from './functions/isDocker';

@ -8,10 +8,13 @@ export const roomTypes = new class roomTypesServer extends RoomTypesCommon {
*
* @param {string} roomType room type (e.g.: c (for channels), d (for direct channels))
* @param {function} callback function that will return the publish's data
*/
*/
setPublish(roomType, callback) {
if (this.roomTypes[roomType] && this.roomTypes[roomType].publish != null) {
throw new Meteor.Error('route-publish-exists', 'Publish for the given type already exists');
throw new Meteor.Error(
'route-publish-exists',
'Publish for the given type already exists',
);
}
if (this.roomTypes[roomType] == null) {
this.roomTypes[roomType] = {};
@ -21,7 +24,10 @@ export const roomTypes = new class roomTypesServer extends RoomTypesCommon {
setRoomFind(roomType, callback) {
if (this.roomTypes[roomType] && this.roomTypes[roomType].roomFind != null) {
throw new Meteor.Error('room-find-exists', 'Room find for the given type already exists');
throw new Meteor.Error(
'room-find-exists',
'Room find for the given type already exists',
);
}
if (this.roomTypes[roomType] == null) {
this.roomTypes[roomType] = {};
@ -34,7 +40,11 @@ export const roomTypes = new class roomTypesServer extends RoomTypesCommon {
}
getRoomName(roomType, roomData) {
return this.roomTypes[roomType] && this.roomTypes[roomType].roomName && this.roomTypes[roomType].roomName(roomData);
return (
this.roomTypes[roomType]
&& this.roomTypes[roomType].roomName
&& this.roomTypes[roomType].roomName(roomData)
);
}
/**
@ -43,8 +53,17 @@ export const roomTypes = new class roomTypesServer extends RoomTypesCommon {
* @param scope Meteor publish scope
* @param {string} roomType room type (e.g.: c (for channels), d (for direct channels))
* @param identifier identifier of the room
*/
*/
runPublish(scope, roomType, identifier) {
return this.roomTypes[roomType] && this.roomTypes[roomType].publish && this.roomTypes[roomType].publish.call(scope, identifier);
return (
this.roomTypes[roomType]
&& this.roomTypes[roomType].publish
&& this.roomTypes[roomType].publish.call(scope, identifier)
);
}
}();
export const searchableRoomTypes = () =>
Object.entries(roomTypes.roomTypes)
.filter((roomType) => roomType[1].includeInRoomSearch())
.map((roomType) => roomType[0]);

@ -28,7 +28,7 @@ addAction('bbb_video', ({ room }) => {
return useMemo(() => (enabled ? {
groups,
id: 'bbb_video',
title: 'BBB Video Call',
title: 'BBB_Video_Call',
icon: 'phone',
template: templateBBB,
order: live ? -1 : 0,

@ -5,6 +5,7 @@ import { css } from '@rocket.chat/css-in-js';
const BreadcrumbsSeparator = () => <Box display='inline-block' fontScale='s2' mi='x4' fontWeight='600' color='neutral-500'>/</Box>;
const BreadcrumbsIcon = ({ name, color, children }) => <Box w='x20' mi='x2' display='inline-flex' justifyContent='center' color={color}>{name ? <Icon size='x20' name={name}/> : children}</Box>;
const BreadcrumbsIconSmall = ({ name, color, children }) => <Box w='x12' display='inline-flex' justifyContent='center' color={color}>{name ? <Icon size='x12' name={name}/> : children}</Box>;
const BreadcrumbsLink = (props) => <BreadcrumbsText
is='a'
@ -25,6 +26,7 @@ const BreadcrumbsLink = (props) => <BreadcrumbsText
const BreadcrumbsText = (props) => <Box display='inline' is='span' mi='x2' color='default' {...props} />;
const BreadcrumbsItem = (props) => <Box mi='neg-x2' display='inline-flex' flexDirection='row' alignItems='center' color='info' fontScale='s2' {...props} />;
const BreadcrumbsTag = (props) => <Box backgroundColor='neutral-400' mis='x8' display='inline-flex' flexDirection='row' alignItems='center' color='neutral-700' fontSize='x12' borderRadius='x4' p='x4' lineHeight='x16' fontWeight='600' height='x20'{...props} />;
const Breadcrumbs = ({ children }) => <Box withTruncatedText mie='x2' display='flex' flexDirection='row' alignItems='center'>{children}</Box>;
@ -32,8 +34,10 @@ Object.assign(Breadcrumbs, {
Text: BreadcrumbsText,
Link: BreadcrumbsLink,
Icon: BreadcrumbsIcon,
IconSmall: BreadcrumbsIconSmall,
Separator: BreadcrumbsSeparator,
Item: BreadcrumbsItem,
Tag: BreadcrumbsTag,
});
export default Breadcrumbs;

@ -0,0 +1,18 @@
import React from 'react';
import GenericModal, { GenericModalDoNotAskAgain } from './GenericModal';
export default {
title: 'components/GenericModal',
component: GenericModal,
};
const func = () => null;
const defaultProps = { onClose: func, onConfirm: func, onCancel: func };
export const _default = () => <GenericModal {...defaultProps} />;
export const Danger = () => <GenericModal {...defaultProps} variant='danger' />;
export const Warning = () => <GenericModal {...defaultProps} variant='warning' />;
export const Success = () => <GenericModal {...defaultProps} variant='success' />;
export const WithDontAskAgain = () => <GenericModalDoNotAskAgain dontAskAgain={{ action: '', label: '' }} {...defaultProps} />;

@ -0,0 +1,77 @@
import { Box, Button, ButtonGroup, Icon, Modal, ButtonProps } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
import { useTranslation } from '../contexts/TranslationContext';
import { withDoNotAskAgain, RequiredModalProps } from './withDoNotAskAgain';
type VariantType = 'danger' | 'warning' | 'info' | 'success';
type GenericModalProps = RequiredModalProps & {
variant?: VariantType;
cancelText?: string;
confirmText?: string;
title?: string;
icon?: string;
onCancel?: () => void;
onClose: () => void;
onConfirm: () => void;
};
const iconMap = {
danger: 'modal-warning',
warning: 'modal-warning',
info: 'info',
success: 'check',
};
const getButtonProps = (variant: VariantType): ButtonProps => {
switch (variant) {
case 'danger':
return { primary: true, danger: true };
case 'warning':
return { primary: true };
default:
return { };
}
};
const GenericModal: FC<GenericModalProps> = ({
variant = 'info',
children,
cancelText,
confirmText,
title,
icon,
onCancel,
onClose,
onConfirm,
dontAskAgain,
...props
}) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
{icon !== null && <Icon color={variant} name={icon ?? iconMap[variant]} size={24}/>}
<Modal.Title>{title ?? t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{children}
</Modal.Content>
<Modal.Footer>
<Box display='flex' flexDirection='row' justifyContent='space-between' alignItems='center'>
{dontAskAgain}
<ButtonGroup align='end' flexGrow={1}>
{onCancel && <Button ghost onClick={onCancel}>{cancelText ?? t('Cancel')}</Button>}
<Button {...getButtonProps(variant)} onClick={onConfirm}>{confirmText ?? t('Ok')}</Button>
</ButtonGroup>
</Box>
</Modal.Footer>
</Modal>;
};
// TODO update withDoNotAskAgain to use onConfirm istead of confirm
export const GenericModalDoNotAskAgain = withDoNotAskAgain<GenericModalProps>(({ confirm, ...props }) => <GenericModal onConfirm={confirm} {...props}/>);
export default GenericModal;

@ -18,6 +18,7 @@ const GenericTable = ({
results,
setParams = () => { },
total,
pagination = true,
...props
}, ref) => {
const t = useTranslation();
@ -69,7 +70,7 @@ const GenericTable = ({
</Table>
</ScrollableContentWrapper>
</Box>
<Pagination
{pagination && <Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
@ -78,7 +79,7 @@ const GenericTable = ({
count={total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
/>
/>}
</>
}
</>;

@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { IconProps, Icon, Button, ButtonGroup } from '@rocket.chat/fuselage';
import { useTranslation } from '../../../contexts/TranslationContext';
import { TranslationKey, useTranslation } from '../../../contexts/TranslationContext';
import { Content } from '..';
type RunAction = () => void;
@ -10,7 +10,7 @@ type ActionOptions = {
mid: string;
id: string;
icon: IconProps['name'];
i18nLabel?: string;
i18nLabel?: TranslationKey;
label?: string;
runAction?: RunAction;
};

@ -17,7 +17,7 @@ export type CustomScrollbarsProps = {
renderTrackHorizontal?: typeof Scrollbars.defaultProps.renderTrackHorizontal;
}
const ScrollableContentWrapper = forwardRef<HTMLElement, CustomScrollbarsProps>(({ children, style, onScroll, renderView, renderTrackHorizontal }, ref) => {
const ScrollableContentWrapper = forwardRef<HTMLElement, CustomScrollbarsProps>(({ children, style, onScroll, renderView }, ref) => {
const scrollbarsStyle = useMemo(() => ({ ...style, ...styleDefault }), [style]) as CSSProperties;
return <Scrollbars
@ -27,12 +27,15 @@ const ScrollableContentWrapper = forwardRef<HTMLElement, CustomScrollbarsProps>(
style={scrollbarsStyle}
onScrollFrame={onScroll}
renderView={renderView}
renderTrackHorizontal={renderTrackHorizontal}
renderTrackHorizontal={(props): React.ReactElement => <div {...props} className='track-horizontal' style={{ display: 'none' }}/>}
renderThumbVertical={
({ style, ...props }): JSX.Element => (
<div
{...props}
style={{ ...style, backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: '7px' }}
style={{ ...style,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderRadius: '7px',
}}
/>
)
}

@ -1,14 +0,0 @@
import React, { useState } from 'react';
import { Avatar, Skeleton } from '@rocket.chat/fuselage';
function BaseAvatar(props) {
const [error, setError] = useState(false);
if (error) {
return <Skeleton variant='rect' {...props} />;
}
return <Avatar onError={setError} {...props}/>;
}
export default BaseAvatar;

@ -0,0 +1,16 @@
import React, { FC, useState } from 'react';
import { Avatar, AvatarProps, Skeleton } from '@rocket.chat/fuselage';
export type BaseAvatarProps = AvatarProps;
const BaseAvatar: FC<BaseAvatarProps> = ({ size, ...props }) => {
const [error, setError] = useState<unknown>(false);
if (error) {
return <Skeleton variant='rect' {...props} />;
}
return <Avatar onError={setError} size={size} {...props}/>;
};
export default BaseAvatar;

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

@ -0,0 +1,22 @@
import React, { FC, memo } from 'react';
import BaseAvatar, { BaseAvatarProps } from './BaseAvatar';
import { useUserAvatarPath } from '../../contexts/AvatarUrlContext';
type UserAvatarProps = Omit<BaseAvatarProps, 'url' | 'title'> & {
username: string;
etag?: string;
url?: string;
};
const UserAvatar: FC<UserAvatarProps> = ({ username, etag, ...rest }) => {
const getUserAvatarPath = useUserAvatarPath();
const {
url = getUserAvatarPath(username, etag),
...props
} = rest;
return <BaseAvatar url={url} title={username} {...props}/>;
};
export default memo(UserAvatar);

@ -1,46 +0,0 @@
import { createContext, useCallback, useContext, useMemo } from 'react';
type ServerContextValue = {
info: {};
absoluteUrl: (path: string) => string;
callMethod: (methodName: string, ...args: any[]) => Promise<any>;
callEndpoint: (httpMethod: 'GET' | 'POST' | 'DELETE', endpoint: string, ...args: any[]) => Promise<any>;
uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise<void>;
getStream: (streamName: string, options?: {}) => <T>(eventName: string, callback: (data: T) => void) => () => void;
};
export const ServerContext = createContext<ServerContextValue>({
info: {},
absoluteUrl: (path) => path,
callMethod: async () => undefined,
callEndpoint: async () => undefined,
uploadToEndpoint: async () => undefined,
getStream: () => () => (): void => undefined,
});
export const useServerInformation = (): {} => useContext(ServerContext).info;
export const useAbsoluteUrl = (): ((path: string) => string) => useContext(ServerContext).absoluteUrl;
export const useMethod = (methodName: string): (...args: any[]) => Promise<any> => {
const { callMethod } = useContext(ServerContext);
return useCallback((...args: any[]) => callMethod(methodName, ...args), [callMethod, methodName]);
};
export const useEndpoint = (httpMethod: 'GET' | 'POST' | 'DELETE', endpoint: string): (...args: any[]) => Promise<any> => {
const { callEndpoint } = useContext(ServerContext);
return useCallback((...args: any[]) => callEndpoint(httpMethod, endpoint, ...args), [callEndpoint, httpMethod, endpoint]);
};
export const useUpload = (endpoint: string): (params: any, formData: any) => Promise<void> => {
const { uploadToEndpoint } = useContext(ServerContext);
return useCallback((params, formData: any) => uploadToEndpoint(endpoint, params, formData), [endpoint, uploadToEndpoint]);
};
export const useStream = (
streamName: string,
options?: {},
): <T>(eventName: string, callback: (data: T) => void) => (() => void) => {
const { getStream } = useContext(ServerContext);
return useMemo(() => getStream(streamName, options), [getStream, streamName, options]);
};

@ -0,0 +1,72 @@
import { createContext, useCallback, useContext, useMemo } from 'react';
import { ServerEndpointMethodOf, ServerEndpointPath, ServerEndpointFunction, ServerEndpointRequestPayload, ServerEndpointFormData, ServerEndpointResponsePayload } from './endpoints';
import { ServerMethodFunction, ServerMethodName, ServerMethodParameters, ServerMethodReturn, ServerMethods } from './methods';
type ServerContextValue = {
info: {};
absoluteUrl: (path: string) => string;
callMethod?: <MethodName extends ServerMethodName>(methodName: MethodName, ...args: ServerMethodParameters<MethodName>) => Promise<ServerMethodReturn<MethodName>>;
callEndpoint?: <
Method extends ServerEndpointMethodOf<Path>,
Path extends ServerEndpointPath
>(httpMethod: Method, endpoint: Path, params: ServerEndpointRequestPayload<Method, Path>, formData?: ServerEndpointFormData<Method, Path>) => Promise<ServerEndpointResponsePayload<Method, Path>>;
uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise<void>;
getStream: (streamName: string, options?: {}) => <T>(eventName: string, callback: (data: T) => void) => () => void;
};
export const ServerContext = createContext<ServerContextValue>({
info: {},
absoluteUrl: (path) => path,
uploadToEndpoint: async () => undefined,
getStream: () => () => (): void => undefined,
});
export const useServerInformation = (): {} => useContext(ServerContext).info;
export const useAbsoluteUrl = (): ((path: string) => string) => useContext(ServerContext).absoluteUrl;
export const useMethod = <MethodName extends keyof ServerMethods>(
methodName: MethodName,
): ServerMethodFunction<MethodName> => {
const { callMethod } = useContext(ServerContext);
return useCallback(
(...args: ServerMethodParameters<MethodName>) => {
if (!callMethod) {
throw new Error(`cannot use useMethod(${ methodName }) hook without a wrapping ServerContext`);
}
return callMethod(methodName, ...args);
},
[callMethod, methodName],
);
};
export const useEndpoint = <
Method extends ServerEndpointMethodOf<Path>,
Path extends ServerEndpointPath
>(httpMethod: Method, endpoint: Path): ServerEndpointFunction<Method, Path> => {
const { callEndpoint } = useContext(ServerContext);
return useCallback((params: ServerEndpointRequestPayload<Method, Path>, formData?: ServerEndpointFormData<Method, Path>) => {
if (!callEndpoint) {
throw new Error(`cannot use useEndpoint(${ httpMethod }, ${ endpoint }) hook without a wrapping ServerContext`);
}
return callEndpoint(httpMethod, endpoint, params, formData);
}, [callEndpoint, endpoint, httpMethod]);
};
export const useUpload = (endpoint: string): (params: any, formData: any) => Promise<void> => {
const { uploadToEndpoint } = useContext(ServerContext);
return useCallback((params, formData: any) => uploadToEndpoint(endpoint, params, formData), [endpoint, uploadToEndpoint]);
};
export const useStream = (
streamName: string,
options?: {},
): <T>(eventName: string, callback: (data: T) => void) => (() => void) => {
const { getStream } = useContext(ServerContext);
return useMemo(() => getStream(streamName, options), [getStream, streamName, options]);
};

@ -0,0 +1,70 @@
import { FollowMessageEndpoint as ChatFollowMessageEndpoint } from './endpoints/v1/chat/followMessage';
import { GetMessageEndpoint as ChatGetMessageEndpoint } from './endpoints/v1/chat/getMessage';
import { UnfollowMessageEndpoint as ChatUnfollowMessageEndpoint } from './endpoints/v1/chat/unfollowMessage';
import { AutocompleteEndpoint as UsersAutocompleteEndpoint } from './endpoints/v1/users/autocomplete';
import { AutocompleteChannelAndPrivateEndpoint as RoomsAutocompleteEndpoint } from './endpoints/v1/rooms/autocompleteChannelAndPrivate';
import { AppearanceEndpoint as LivechatAppearanceEndpoint } from './endpoints/v1/livechat/appearance';
import { ListEndpoint as CustomUserStatusListEndpoint } from './endpoints/v1/custom-user-status/list';
import { ExternalComponentsEndpoint as AppsExternalComponentsEndpoint } from './endpoints/apps/externalComponents';
import { ManualRegisterEndpoint as CloudManualRegisterEndpoint } from './endpoints/v1/cloud/manualRegister';
import { FilesEndpoint as GroupsFilesEndpoint } from './endpoints/v1/groups/files';
import { FilesEndpoint as ImFilesEndpoint } from './endpoints/v1/im/files';
import { AddRoomsEndpoint as TeamsAddRoomsEndpoint } from './endpoints/v1/teams/addRooms';
import { FilesEndpoint as ChannelsFilesEndpoint } from './endpoints/v1/channels/files';
import { ListEndpoint as EmojiCustomListEndpoint } from './endpoints/v1/emoji-custom/list';
import { GetDiscussionsEndpoint as ChatGetDiscussionsEndpoint } from './endpoints/v1/chat/getDiscussions';
import { GetThreadsListEndpoint as ChatGetThreadsListEndpoint } from './endpoints/v1/chat/getThreadsList';
import { LivechatVisitorInfoEndpoint } from './endpoints/v1/livechat/visitorInfo';
export type ServerEndpoints = {
'chat.getMessage': ChatGetMessageEndpoint;
'chat.followMessage': ChatFollowMessageEndpoint;
'chat.unfollowMessage': ChatUnfollowMessageEndpoint;
'cloud.manualRegister': CloudManualRegisterEndpoint;
'chat.getDiscussions': ChatGetDiscussionsEndpoint;
'chat.getThreadsList': ChatGetThreadsListEndpoint;
'emoji-custom.list': EmojiCustomListEndpoint;
'channels.files': ChannelsFilesEndpoint;
'im.files': ImFilesEndpoint;
'groups.files': GroupsFilesEndpoint;
'users.autocomplete': UsersAutocompleteEndpoint;
'livechat/appearance': LivechatAppearanceEndpoint;
'custom-user-status.list': CustomUserStatusListEndpoint;
'/apps/externalComponents': AppsExternalComponentsEndpoint;
'rooms.autocomplete.channelAndPrivate': RoomsAutocompleteEndpoint;
'teams.addRooms': TeamsAddRoomsEndpoint;
'livechat/visitors.info': LivechatVisitorInfoEndpoint;
};
export type ServerEndpointPath = keyof ServerEndpoints;
export type ServerEndpointMethodOf<Path extends ServerEndpointPath> = keyof ServerEndpoints[Path] & ('GET' | 'POST' | 'DELETE');
type ServerEndpoint<
Method extends ServerEndpointMethodOf<Path>,
Path extends ServerEndpointPath
> = ServerEndpoints[Path][Method] extends (...args: any[]) => any
? ServerEndpoints[Path][Method]
: (...args: any[]) => any;
export type ServerEndpointRequestPayload<
Method extends ServerEndpointMethodOf<Path>,
Path extends ServerEndpointPath
> = Parameters<ServerEndpoint<Method, Path>>[0];
export type ServerEndpointFormData<
Method extends ServerEndpointMethodOf<Path>,
Path extends ServerEndpointPath
> = Parameters<ServerEndpoint<Method, Path>>[1];
export type ServerEndpointResponsePayload<
Method extends ServerEndpointMethodOf<Path>,
Path extends ServerEndpointPath
> = ReturnType<ServerEndpoint<Method, Path>>;
export type ServerEndpointFunction<
Method extends ServerEndpointMethodOf<Path>,
Path extends ServerEndpointPath
> = {
(params: ServerEndpointRequestPayload<Method, Path>): Promise<ServerEndpointResponsePayload<Method, Path>>;
(params: ServerEndpointRequestPayload<Method, Path>, formData: ServerEndpointFormData<Method, Path>): Promise<ServerEndpointResponsePayload<Method, Path>>;
};

@ -0,0 +1,5 @@
import { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent';
export type ExternalComponentsEndpoint = {
GET: (params: Record<string, never>) => { externalComponents: IExternalComponent[] };
};

@ -0,0 +1,15 @@
import { IMessage } from '../../../../../../definition/IMessage';
import { IRoom } from '../../../../../../definition/IRoom';
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi';
export type FilesEndpoint = {
GET: (params: {
roomId: IRoom['_id'];
count: number;
sort: string;
query: string;
}) => {
files: ObjectFromApi<IMessage>[];
total: number;
};
};

@ -0,0 +1,9 @@
import { IMessage } from '../../../../../../definition/IMessage';
export type FollowMessageEndpoint = {
POST: (params: { mid: IMessage['_id'] }) => {
success: true;
statusCode: 200;
body: {};
};
}

@ -0,0 +1,15 @@
import { IRoom } from '../../../../../../definition/IRoom';
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi';
import { IMessage } from '../../../../../../definition/IMessage';
export type GetDiscussionsEndpoint = {
GET: (params: {
roomId: IRoom['_id'];
text?: string;
offset: number;
count: number;
}) => {
messages: ObjectFromApi<IMessage>[];
total: number;
};
};

@ -0,0 +1,7 @@
import { IMessage } from '../../../../../../definition/IMessage';
export type GetMessageEndpoint = {
GET: (params: { msgId: IMessage['_id'] }) => {
message: IMessage;
};
};

@ -0,0 +1,16 @@
import { IRoom } from '../../../../../../definition/IRoom';
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi';
import { IMessage } from '../../../../../../definition/IMessage';
export type GetThreadsListEndpoint = {
GET: (params: {
rid: IRoom['_id'];
type: 'unread' | 'following' | 'all';
text?: string;
offset: number;
count: number;
}) => {
threads: ObjectFromApi<IMessage>[];
total: number;
};
};

@ -0,0 +1,9 @@
import { IMessage } from '../../../../../../definition/IMessage';
export type UnfollowMessageEndpoint = {
POST: (params: { mid: IMessage['_id'] }) => {
success: true;
statusCode: 200;
body: {};
};
}

@ -0,0 +1,3 @@
export type ManualRegisterEndpoint = {
POST: (params: Record<string, never>, formData: { cloudBlob: string }) => void;
};

@ -0,0 +1,5 @@
export type ListEndpoint = {
GET: (params: { query: string }) => {
statuses: unknown[];
};
};

@ -0,0 +1,14 @@
type EmojiDescriptor = {
_id: string;
name: string;
aliases: string[];
extension: string;
};
export type ListEndpoint = {
GET: (params: { query: string }) => {
emojis?: {
update: EmojiDescriptor[];
};
};
};

@ -0,0 +1,15 @@
import { IMessage } from '../../../../../../definition/IMessage';
import { IRoom } from '../../../../../../definition/IRoom';
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi';
export type FilesEndpoint = {
GET: (params: {
roomId: IRoom['_id'];
count: number;
sort: string;
query: string;
}) => {
files: ObjectFromApi<IMessage>[];
total: number;
};
};

@ -0,0 +1,15 @@
import { IMessage } from '../../../../../../definition/IMessage';
import { IRoom } from '../../../../../../definition/IRoom';
import { ObjectFromApi } from '../../../../../../definition/ObjectFromApi';
export type FilesEndpoint = {
GET: (params: {
roomId: IRoom['_id'];
count: number;
sort: string;
query: string;
}) => {
files: ObjectFromApi<IMessage>[];
total: number;
};
};

@ -0,0 +1,8 @@
import { ISetting } from '../../../../../../definition/ISetting';
export type AppearanceEndpoint = {
GET: (params: Record<string, never>) => {
success: boolean;
appearance: ISetting[];
};
};

@ -0,0 +1,10 @@
export type LivechatVisitorInfoEndpoint = {
GET: (visitorId: string) => {
success: boolean;
visitor: {
visitorEmails: Array<{
address: string;
}>;
};
};
};

@ -0,0 +1,5 @@
import { IRoom } from '../../../../../../definition/IRoom';
export type AutocompleteChannelAndPrivateEndpoint = {
GET: (params: { selector: string }) => { items: IRoom[] };
};

@ -0,0 +1,9 @@
import { IRoom } from '../../../../../../definition/IRoom';
export type AddRoomsEndpoint = {
POST: (params: { rooms: IRoom['_id'][]; teamId: string }) => {
success: true;
statusCode: 200;
body: IRoom[];
};
};

@ -0,0 +1,5 @@
import { IUser } from '../../../../../../definition/IUser';
export type AutocompleteEndpoint = {
GET: (params: { selector: string }) => { items: IUser[] };
};

@ -0,0 +1,3 @@
export * from './ServerContext';
export * from './endpoints';
export * from './methods';

@ -0,0 +1,140 @@
import { FollowMessageMethod } from './methods/followMessage';
import { RoomNameExistsMethod } from './methods/roomNameExists';
import { SaveRoomSettingsMethod } from './methods/saveRoomSettings';
import { SaveSettingsMethod } from './methods/saveSettings';
import { SaveUserPreferencesMethod } from './methods/saveUserPreferences';
import { UnfollowMessageMethod } from './methods/unfollowMessage';
export type ServerMethods = {
'2fa:checkCodesRemaining': (...args: any[]) => any;
'2fa:disable': (...args: any[]) => any;
'2fa:enable': (...args: any[]) => any;
'2fa:regenerateCodes': (...args: any[]) => any;
'2fa:validateTempToken': (...args: any[]) => any;
'addOAuthApp': (...args: any[]) => any;
'addOAuthService': (...args: any[]) => any;
'addUsersToRoom': (...args: any[]) => any;
'apps/go-enable': (...args: any[]) => any;
'apps/is-enabled': (...args: any[]) => any;
'authorization:addPermissionToRole': (...args: any[]) => any;
'authorization:addUserToRole': (...args: any[]) => any;
'authorization:deleteRole': (...args: any[]) => any;
'authorization:removeRoleFromPermission': (...args: any[]) => any;
'authorization:removeUserFromRole': (...args: any[]) => any;
'authorization:saveRole': (...args: any[]) => any;
'bbbEnd': (...args: any[]) => any;
'bbbJoin': (...args: any[]) => any;
'blockUser': (...args: any[]) => any;
'checkUsernameAvailability': (...args: any[]) => any;
'cleanRoomHistory': (...args: any[]) => any;
'clearIntegrationHistory': (...args: any[]) => any;
'cloud:checkRegisterStatus': (...args: any[]) => any;
'cloud:checkUserLoggedIn': (...args: any[]) => any;
'cloud:connectWorkspace': (...args: any[]) => any;
'cloud:disconnectWorkspace': (...args: any[]) => any;
'cloud:finishOAuthAuthorization': (...args: any[]) => any;
'cloud:getOAuthAuthorizationUrl': (...args: any[]) => any;
'cloud:getWorkspaceRegisterData': (...args: any[]) => any;
'cloud:logout': (...args: any[]) => any;
'cloud:registerWorkspace': (...args: any[]) => any;
'cloud:syncWorkspace': (...args: any[]) => any;
'deleteCustomSound': (...args: any[]) => any;
'deleteCustomUserStatus': (...args: any[]) => any;
'deleteFileMessage': (...args: any[]) => any;
'deleteOAuthApp': (...args: any[]) => any;
'deleteUserOwnAccount': (...args: any[]) => any;
'e2e.resetOwnE2EKey': (...args: any[]) => any;
'eraseRoom': (...args: any[]) => any;
'followMessage': FollowMessageMethod;
'getAvatarSuggestion': (...args: any[]) => any;
'getSetupWizardParameters': (...args: any[]) => any;
'getUsersOfRoom': (...args: any[]) => any;
'hideRoom': (...args: any[]) => any;
'ignoreUser': (...args: any[]) => any;
'insertOrUpdateSound': (...args: any[]) => any;
'insertOrUpdateUserStatus': (...args: any[]) => any;
'instances/get': (...args: any[]) => any;
'jitsi:generateAccessToken': (...args: any[]) => any;
'jitsi:updateTimeout': (...args: any[]) => any;
'leaveRoom': (...args: any[]) => any;
'license:getTags': (...args: any[]) => any;
'livechat:addMonitor': (...args: any[]) => any;
'livechat:changeLivechatStatus': (...args: any[]) => any;
'livechat:closeRoom': (...args: any[]) => any;
'livechat:discardTranscript': (...args: any[]) => any;
'livechat:facebook': (...args: any[]) => any;
'livechat:getAgentOverviewData': (...args: any[]) => any;
'livechat:getAnalyticsChartData': (...args: any[]) => any;
'livechat:getAnalyticsOverviewData': (...args: any[]) => any;
'livechat:getRoutingConfig': (...args: any[]) => any;
'livechat:removeAllClosedRooms': (...args: any[]) => any;
'livechat:removeBusinessHour': (...args: any[]) => any;
'livechat:removeCustomField': (...args: any[]) => any;
'livechat:removeMonitor': (...args: any[]) => any;
'livechat:removePriority': (...args: any[]) => any;
'livechat:removeRoom': (...args: any[]) => any;
'livechat:removeTag': (...args: any[]) => any;
'livechat:removeTrigger': (...args: any[]) => any;
'livechat:removeUnit': (...args: any[]) => any;
'livechat:requestTranscript': (...args: any[]) => any;
'livechat:returnAsInquiry': (...args: any[]) => any;
'livechat:sendTranscript': (...args: any[]) => any;
'livechat:transfer': (...args: any[]) => any;
'livechat:saveAgentInfo': (...args: any[]) => any;
'livechat:saveAppearance': (...args: any[]) => any;
'livechat:saveBusinessHour': (...args: any[]) => any;
'livechat:saveCustomField': (...args: any[]) => any;
'livechat:saveDepartment': (...args: any[]) => any;
'livechat:saveIntegration': (...args: any[]) => any;
'livechat:savePriority': (...args: any[]) => any;
'livechat:saveTag': (...args: any[]) => any;
'livechat:saveTrigger': (...args: any[]) => any;
'livechat:saveUnit': (...args: any[]) => any;
'livechat:webhookTest': (...args: any[]) => any;
'logoutOtherClients': (...args: any[]) => any;
'Mailer.sendMail': (...args: any[]) => any;
'muteUserInRoom': (...args: any[]) => any;
'personalAccessTokens:generateToken': (...args: any[]) => any;
'personalAccessTokens:regenerateToken': (...args: any[]) => any;
'personalAccessTokens:removeToken': (...args: any[]) => any;
'readMessages': (...args: any[]) => any;
'refreshClients': (...args: any[]) => any;
'refreshOAuthService': (...args: any[]) => any;
'registerUser': (...args: any[]) => any;
'removeOAuthService': (...args: any[]) => any;
'removeWebdavAccount': (...args: any[]) => any;
'replayOutgoingIntegration': (...args: any[]) => any;
'requestDataDownload': (...args: any[]) => any;
'resetPassword': (...args: any[]) => any;
'roomNameExists': RoomNameExistsMethod;
'saveCannedResponse': (...args: any[]) => any;
'saveRoomSettings': SaveRoomSettingsMethod;
'saveSettings': SaveSettingsMethod;
'saveUserPreferences': SaveUserPreferencesMethod;
'saveUserProfile': (...args: any[]) => any;
'sendConfirmationEmail': (...args: any[]) => any;
'sendInvitationEmail': (...args: any[]) => any;
'setAdminStatus': (...args: any[]) => any;
'setAsset': (...args: any[]) => any;
'setAvatarFromService': (...args: any[]) => any;
'setUsername': (...args: any[]) => any;
'setUserPassword': (...args: any[]) => any;
'toggleFavorite': (...args: any[]) => any;
'unblockUser': (...args: any[]) => any;
'unfollowMessage': UnfollowMessageMethod;
'unmuteUserInRoom': (...args: any[]) => any;
'unreadMessages': (...args: any[]) => any;
'unsetAsset': (...args: any[]) => any;
'updateIncomingIntegration': (...args: any[]) => any;
'updateOAuthApp': (...args: any[]) => any;
'updateOutgoingIntegration': (...args: any[]) => any;
'uploadCustomSound': (...args: any[]) => any;
};
export type ServerMethodName = keyof ServerMethods;
export type ServerMethodParameters<MethodName extends ServerMethodName> = Parameters<ServerMethods[MethodName]>;
export type ServerMethodReturn<MethodName extends ServerMethodName> = ReturnType<ServerMethods[MethodName]>;
export type ServerMethodFunction<MethodName extends ServerMethodName> = (...args: ServerMethodParameters<MethodName>) => Promise<ServerMethodReturn<MethodName>>;

@ -0,0 +1,3 @@
import { IMessage } from '../../../../definition/IMessage';
export type FollowMessageMethod = (options: { mid: IMessage['_id'] }) => false | undefined;

@ -0,0 +1,3 @@
import { IRoom } from '../../../../definition/IRoom';
export type RoomNameExistsMethod = (name: IRoom['name']) => boolean;

@ -0,0 +1,32 @@
import { IRoom } from '../../../../definition/IRoom';
type RoomSettings = {
'roomAvatar': unknown;
'featured': unknown;
'roomName': unknown;
'roomTopic': unknown;
'roomAnnouncement': unknown;
'roomCustomFields': unknown;
'roomDescription': unknown;
'roomType': unknown;
'readOnly': unknown;
'reactWhenReadOnly': unknown;
'systemMessages': unknown;
'default': unknown;
'joinCode': unknown;
'tokenpass': unknown;
'streamingOptions': unknown;
'retentionEnabled': unknown;
'retentionMaxAge': unknown;
'retentionExcludePinned': unknown;
'retentionFilesOnly': unknown;
'retentionIgnoreThreads': unknown;
'retentionOverrideGlobal': unknown;
'encrypted': boolean;
'favorite': unknown;
};
export type SaveRoomSettingsMethod = {
(rid: IRoom['_id'], settings: Partial<RoomSettings>): { result: true; rid: IRoom['_id'] };
<RoomSettingName extends keyof RoomSettings>(rid: IRoom['_id'], setting: RoomSettingName, value: RoomSettings[RoomSettingName]): { result: true; rid: IRoom['_id'] };
}

@ -0,0 +1,9 @@
import { ISetting } from '../../../../definition/ISetting';
type SettingChange = {
_id: ISetting['_id'];
value: unknown;
editor?: unknown;
};
export type SaveSettingsMethod = (changes: SettingChange[]) => true;

@ -0,0 +1,37 @@
type UserPreferences = {
language: string;
newRoomNotification: string;
newMessageNotification: string;
clockMode: number;
useEmojis: boolean;
convertAsciiEmoji: boolean;
saveMobileBandwidth: boolean;
collapseMediaByDefault: boolean;
autoImageLoad: boolean;
emailNotificationMode: string;
unreadAlert: boolean;
notificationsSoundVolume: number;
desktopNotifications: string;
audioNotifications: string;
mobileNotifications: string;
enableAutoAway: boolean;
highlights: string[];
messageViewMode: number;
hideUsernames: boolean;
hideRoles: boolean;
hideAvatars: boolean;
hideFlexTab: boolean;
sendOnEnter: string;
idleTimeLimit: number;
sidebarShowFavorites: boolean;
sidebarShowUnread: boolean;
sidebarSortby: string;
sidebarViewMode: string;
sidebarHideAvatar: boolean;
sidebarGroupByType: boolean;
sidebarShowDiscussion: boolean;
muteFocusedConversations: boolean;
dontAskAgainList: { action: string; label: string }[];
};
export type SaveUserPreferencesMethod = (preferences: Partial<UserPreferences>) => boolean;

@ -0,0 +1,3 @@
import { IMessage } from '../../../../definition/IMessage';
export type UnfollowMessageMethod = (options: { mid: IMessage['_id'] }) => false | undefined;

@ -1,18 +1,22 @@
import { createContext, useContext } from 'react';
import type keys from '../../packages/rocketchat-i18n/i18n/en.i18n.json';
export type TranslationLanguage = {
name: string;
en: string;
key: string;
};
export type TranslationKey = keyof typeof keys;
export type TranslationContextValue = {
languages: TranslationLanguage[];
language: TranslationLanguage['key'];
loadLanguage: (language: TranslationLanguage['key']) => Promise<void>;
translate: {
(key: string, ...replaces: unknown[]): string;
has: (key: string) => boolean;
(key: TranslationKey, ...replaces: unknown[]): string;
has: (key: TranslationKey) => boolean;
};
};

@ -1,17 +1,21 @@
import { useCallback, useEffect } from 'react';
import { useEndpoint } from '../contexts/ServerContext';
import { ServerEndpointPath, ServerEndpointRequestPayload, ServerEndpointResponsePayload, ServerEndpoints, useEndpoint } from '../contexts/ServerContext';
import { useToastMessageDispatch } from '../contexts/ToastMessagesContext';
import { AsyncState, useAsyncState } from './useAsyncState';
const defaultParams = {};
export const useEndpointData = <T>(
endpoint: string,
params: Record<string, unknown> = defaultParams,
initialValue?: T | (() => T),
): AsyncState<T> & { reload: () => void } => {
const { resolve, reject, reset, ...state } = useAsyncState<T>(initialValue);
type ServerGetEndpointPaths = {
[K in ServerEndpointPath]: ServerEndpoints[K] extends { GET: any } ? K : never
};
export const useEndpointData = <_T, Path extends ServerGetEndpointPaths[keyof ServerGetEndpointPaths]>(
endpoint: Path,
params: ServerEndpointRequestPayload<'GET', Path> = defaultParams,
initialValue?: ServerEndpointResponsePayload<'GET', Path> | (() => ServerEndpointResponsePayload<'GET', Path>),
): AsyncState<ServerEndpointResponsePayload<'GET', Path>> & { reload: () => void } => {
const { resolve, reject, reset, ...state } = useAsyncState<ServerEndpointResponsePayload<'GET', Path>>(initialValue);
const dispatchToastMessage = useToastMessageDispatch();
const getData = useEndpoint('GET', endpoint);

@ -1,13 +1,13 @@
import { useCallback, useEffect } from 'react';
import { useMethod } from '../contexts/ServerContext';
import { ServerMethods, useMethod } from '../contexts/ServerContext';
import { useToastMessageDispatch } from '../contexts/ToastMessagesContext';
import { AsyncState, useAsyncState } from './useAsyncState';
const defaultArgs: unknown[] = [];
export const useMethodData = <T>(
methodName: string,
methodName: keyof ServerMethods,
args: any[] = defaultArgs,
initialValue?: T | (() => T),
): AsyncState<T> & { reload: () => void } => {

@ -1,9 +1,10 @@
import { useEffect } from 'react';
import { ServerMethods } from '../contexts/ServerContext';
import { AsyncState } from './useAsyncState';
import { useMethodData } from './useMethodData';
export const usePolledMethodData = <T>(methodName: string, args: any[] = [], intervalMs: number): AsyncState<T> & { reload: () => void } => {
export const usePolledMethodData = <T>(methodName: keyof ServerMethods, args: any[] = [], intervalMs: number): AsyncState<T> & { reload: () => void } => {
const { reload, ...state } = useMethodData<T>(methodName, args);
useEffect(() => {

@ -16,6 +16,10 @@ export const useRoomIcon = (room: IRoom, small = true): JSX.Element | { name: st
return { name: 'baloons' };
}
if (room.teamMain) {
return { name: room.t === 'p' ? 'team-lock' : 'team' };
}
switch (room.t) {
case 'p':
return { name: 'hashtag-lock' };

@ -21,4 +21,5 @@ import './startup';
import './views/admin';
import './views/login';
import './views/room/adapters';
import './views/teams';
import './adapters';

@ -6,6 +6,7 @@ import './cssVars';
Object.fromEntries = Object.fromEntries || function fromEntries(iterable) {
return [...iterable].reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {});
};
(function(arr) {
arr.forEach(function(item) {
if (item.hasOwnProperty('remove')) {

@ -91,7 +91,9 @@ const SettingsProvider: FunctionComponent<SettingsProviderProps> = ({
);
const saveSettings = useMethod('saveSettings');
const dispatch = useCallback((changes) => saveSettings(changes), [saveSettings]);
const dispatch = useCallback(async (changes) => {
await saveSettings(changes);
}, [saveSettings]);
const contextValue = useMemo<SettingsContextValue>(() => ({
hasPrivateAccess,

@ -129,6 +129,7 @@ export const CreateChannel = ({
export default memo(({
onClose,
teamId = '',
}) => {
const createChannel = useEndpointActionExperimental('POST', 'channels.create');
const createPrivateChannel = useEndpointActionExperimental('POST', 'groups.create');
@ -204,6 +205,7 @@ export default memo(({
name,
members: users,
readOnly,
...teamId && { teamId },
extraData: {
description,
broadcast,
@ -214,14 +216,25 @@ export default memo(({
if (type) {
roomData = await createPrivateChannel(params);
goToRoom(roomData.group._id);
!teamId && goToRoom(roomData.group._id);
} else {
roomData = await createChannel(params);
goToRoom(roomData.channel._id);
!teamId && goToRoom(roomData.channel._id);
}
onClose();
}, [broadcast, createChannel, createPrivateChannel, description, encrypted, name, onClose, readOnly, type, users]);
}, [broadcast,
createChannel,
createPrivateChannel,
description,
encrypted,
name,
onClose,
readOnly,
teamId,
type,
users,
]);
return <CreateChannel
values={values}

@ -8,10 +8,13 @@ import { useAtLeastOnePermission, usePermission } from '../../../contexts/Author
import { useSetting } from '../../../contexts/SettingsContext';
import { useSetModal } from '../../../contexts/ModalContext';
import CreateChannel from '../CreateChannel';
import CreateTeamModal from '../../../views/teams/modals/CreateTeamModal';
import CreateRoomListItem from './CreateRoomListItem';
const CREATE_CHANNEL_PERMISSIONS = ['create-c', 'create-p'];
const CREATE_TEAM_PERMISSIONS = ['create-team'];
const CREATE_DISCUSSION_PERMISSIONS = ['start-discussion', 'start-discussion-other-user'];
const style = {
@ -58,10 +61,12 @@ function CreateRoomList() {
const t = useTranslation();
const canCreateChannel = useAtLeastOnePermission(CREATE_CHANNEL_PERMISSIONS);
const canCreateTeam = useAtLeastOnePermission(CREATE_TEAM_PERMISSIONS);
const canCreateDirectMessages = usePermission('create-d');
const canCreateDiscussion = useAtLeastOnePermission(CREATE_DISCUSSION_PERMISSIONS);
const createChannel = useReactModal(CreateChannel);
const createTeam = useReactModal(CreateTeamModal);
const createDirectMessage = useAction(t('Direct_Messages'), 'CreateDirectMessage');
const createDiscussion = useAction(t('Discussion_title'), 'CreateDiscussion');
@ -74,6 +79,7 @@ function CreateRoomList() {
<ul className='rc-popover__list'>
<Margins block='x8'>
{canCreateChannel && <CreateRoomListItem icon='hashtag' text={t('Channel')} action={createChannel} />}
{canCreateTeam && <CreateRoomListItem icon='team' text={t('Team')} action={createTeam} />}
{canCreateDirectMessages && <CreateRoomListItem icon='balloon' text={t('Direct_Messages')} action={createDirectMessage} />}
{discussionEnabled && canCreateDiscussion && <CreateRoomListItem icon='discussion' text={t('Discussion')} action={createDiscussion} />}
</Margins>

@ -26,6 +26,7 @@ export const useRoomList = (): Array<ISubscription> => {
useEffect(() => {
setRoomList(() => {
const favorite = new Set();
const team = new Set();
const omnichannel = new Set();
const unread = new Set();
const _private = new Set();
@ -44,6 +45,10 @@ export const useRoomList = (): Array<ISubscription> => {
return favorite.add(room);
}
if (room.teamMain) {
return team.add(room);
}
if (showDiscussion && room.prid) {
return discussion.add(room);
}
@ -80,6 +85,7 @@ export const useRoomList = (): Array<ISubscription> => {
showOmnichannel && onHold.size && groups.set('On_Hold_Chats', onHold);
sidebarShowUnread && unread.size && groups.set('Unread', unread);
favoritesEnabled && favorite.size && groups.set('Favorites', favorite);
team.size && groups.set('Teams', team);
showDiscussion && discussion.size && groups.set('Discussions', discussion);
sidebarGroupByType && _private.size && groups.set('Private', _private);
sidebarGroupByType && _public.size && groups.set('Public', _public);

@ -173,6 +173,23 @@ declare module '@rocket.chat/fuselage' {
Item: ForwardRefExoticComponent<AccordionItemProps>;
};
type AutoCompleteProps = {
value: unknown[];
filter: string;
setFilter?: (filter: string) => void;
options?: { label: string; value: unknown }[];
renderItem: ElementType;
renderSelected?: ElementType;
onChange: (value: unknown, action: 'remove' | undefined) => void;
getLabel?: (option: { label: string; value: unknown }) => string;
getValue?: (option: { label: string; value: unknown }) => unknown;
renderEmpty?: ElementType;
placeholder?: string;
error?: boolean;
disabled?: boolean;
};
export const AutoComplete: FC<AutoCompleteProps>;
type AvatarProps = Omit<BoxProps, 'title' | 'size'> & {
title?: string;
size?: 'x16' | 'x18' | 'x20' | 'x24' | 'x28' | 'x32' | 'x36' | 'x40' | 'x48' | 'x124' | 'x200' | 'x332';
@ -237,6 +254,7 @@ declare module '@rocket.chat/fuselage' {
export const Field: ForwardRefExoticComponent<FieldProps> & {
Row: ForwardRefExoticComponent<BoxProps>;
Label: ForwardRefExoticComponent<BoxProps>;
Description: ForwardRefExoticComponent<BoxProps>;
Hint: ForwardRefExoticComponent<BoxProps>;
Error: ForwardRefExoticComponent<BoxProps>;
};
@ -274,6 +292,31 @@ declare module '@rocket.chat/fuselage' {
type NumberInputProps = BoxProps;
export const NumberInput: ForwardRefExoticComponent<NumberInputProps>;
type OptionsProps = BoxProps & {
multiple?: boolean;
options: [unknown, string, boolean?][];
cursor: number;
renderItem?: ElementType;
renderEmpty?: ElementType;
onSelect: (option: [unknown, string]) => void;
};
export const Options: ForwardRefExoticComponent<OptionsProps> & {
AvatarSize: AvatarProps['size'];
};
type OptionProps = {
id?: string;
avatar?: ReactNode;
label?: string;
focus?: boolean;
selected?: boolean;
icon?: string;
className?: BoxProps['className'];
title?: string;
value?: any;
};
export const Option: ForwardRefExoticComponent<OptionProps>;
type PaginationProps = BoxProps & {
count: number;
current?: number;
@ -371,20 +414,6 @@ declare module '@rocket.chat/fuselage' {
export const Divider: ForwardRefExoticComponent<BoxProps>;
type OptionProps = {
id?: string;
avatar?: typeof Avatar;
label?: string;
focus?: boolean;
selected?: boolean;
icon?: string;
className?: BoxProps['className'];
title?: string;
value?: any;
};
export const Option: ForwardRefExoticComponent<OptionProps>;
export type MenuProps = Omit<ActionButtonProps, 'icon'> & {
icon?: string;
options: {

@ -138,5 +138,6 @@ declare module 'meteor/kadira:flow-router' {
export const FlowRouter: Router & {
Route: typeof Route;
Router: typeof Router;
goToRoomById: (rid: unknown) => void;
};
}

@ -0,0 +1,67 @@
import React from 'react';
import InfoPanel, { RetentionPolicyCallout } from '.';
export default {
title: 'components/InfoPanel',
component: InfoPanel,
};
const room = {
fname: 'rocketchat-frontend-team',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam mollis nisi vel arcu bibendum vehicula. Integer vitae suscipit libero',
announcement: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam mollis nisi vel arcu bibendum vehicula. Integer vitae suscipit libero',
topic: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam mollis nisi vel arcu bibendum vehicula. Integer vitae suscipit libero',
};
export const Default = () => <InfoPanel>
<InfoPanel.Avatar />
<InfoPanel.Section>
<InfoPanel.Title title={room.fname} icon={'hashtag'}/>
</InfoPanel.Section>
<InfoPanel.Section>
<InfoPanel.Field>
<InfoPanel.Label>Description</InfoPanel.Label>
<InfoPanel.Text>{room.description}</InfoPanel.Text>
</InfoPanel.Field>
<InfoPanel.Field>
<InfoPanel.Label>Announcement</InfoPanel.Label>
<InfoPanel.Text>{room.announcement}</InfoPanel.Text>
</InfoPanel.Field>
<InfoPanel.Field>
<InfoPanel.Label>Topic</InfoPanel.Label>
<InfoPanel.Text>{room.topic}</InfoPanel.Text>
</InfoPanel.Field>
</InfoPanel.Section>
<InfoPanel.Section>
<RetentionPolicyCallout maxAgeDefault={30} filesOnlyDefault={false} excludePinnedDefault={true} />
</InfoPanel.Section>
</InfoPanel>;
// export const Archived = () => <VerticalBar height={800}>
// <RoomInfo
// {...room}
// icon='lock'
// onClickHide={alert}
// onClickLeave={alert}
// onClickEdit={alert}
// onClickDelete={alert}
// archived
// />
// </VerticalBar>;
// export const Broadcast = () => <VerticalBar height={800}>
// <RoomInfo
// {...room}
// icon='lock'
// onClickHide={alert}
// onClickLeave={alert}
// onClickEdit={alert}
// onClickDelete={alert}
// broadcast
// />
// </VerticalBar>;

@ -0,0 +1,62 @@
import React, { FC, ReactNode } from 'react';
import { Box, Icon, BoxProps, Button, ButtonProps, ButtonGroup, ButtonGroupProps } from '@rocket.chat/fuselage';
import { css } from '@rocket.chat/css-in-js';
type TitleProps = {
title: string;
icon: string | ReactNode;
}
const wordBreak = css`
word-break: break-word;
`;
const InfoPanel: FC = ({ children }) => <Box flexGrow={1} mb='neg-x24'>{children}</Box>;
const Section: FC<BoxProps> = (props) => <Box mb='x24' {...props} />;
const Title: FC<TitleProps> = ({ title, icon }) => <Box display='flex' title={title} flexShrink={0} alignItems='center' fontScale='s2' color='default' withTruncatedText>
{
typeof icon === 'string'
? <Icon name={icon} size='x22' />
: icon
}
<Box mis='x16' flexGrow={1} withTruncatedText>{title}</Box>
</Box>;
const Label: FC<BoxProps> = (props) => <Box mb='x8' fontScale='p2' color='default' {...props} />;
const Text: FC<BoxProps> = (props) => <Box
mb='x8'
fontScale='p1'
color='hint'
withTruncatedText
className={wordBreak}
{...props}
/>;
const Action: FC<ButtonProps & { icon: string; label: string }> = ({ label, icon, ...props }) => <Button title={label} aria-label={label} {...props} mi='x4'>
<Icon name={icon} size='x20' mie='x4' />
{label}
</Button>;
const ActionGroup: FC<ButtonGroupProps> = (props) => <Section><ButtonGroup flexShrink={0} flexWrap='nowrap' withTruncatedText justifyContent='center' {...props}/></Section>;
const Field: FC = ({ children }) => <Box mb='x12'>{children}</Box>;
const Avatar: FC = ({ children }) => <Section display='flex' justifyContent='center'>
{children}
</Section>;
Object.assign(InfoPanel, {
Title,
Label,
Text,
Avatar,
Field,
Action,
Section,
ActionGroup,
});
export default InfoPanel;

@ -0,0 +1,22 @@
import React, { FC } from 'react';
import { Callout } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
type RetentionPolicyCalloutProps = {
filesOnlyDefault: boolean;
excludePinnedDefault: boolean;
maxAgeDefault: number;
}
const RetentionPolicyCallout: FC<RetentionPolicyCalloutProps> = ({ filesOnlyDefault, excludePinnedDefault, maxAgeDefault }) => {
const t = useTranslation();
return <Callout type='warning'>
{filesOnlyDefault && excludePinnedDefault && <p>{t('RetentionPolicy_RoomWarning_FilesOnly', { time: maxAgeDefault })}</p>}
{filesOnlyDefault && !excludePinnedDefault && <p>{t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time: maxAgeDefault })}</p>}
{!filesOnlyDefault && excludePinnedDefault && <p>{t('RetentionPolicy_RoomWarning', { time: maxAgeDefault })}</p>}
{!filesOnlyDefault && !excludePinnedDefault && <p>{t('RetentionPolicy_RoomWarning_Unpinned', { time: maxAgeDefault })}</p>}
</Callout>;
};
export default RetentionPolicyCallout;

@ -0,0 +1,6 @@
import InfoPanel from './InfoPanel';
import RetentionPolicyCallout from './RetentionPolicyCallout';
export { RetentionPolicyCallout };
export default InfoPanel;

@ -74,7 +74,7 @@ const PasteStep: FC<PasteStepProps> = ({ onBackButtonClick, onFinish }) => {
<ButtonGroup>
<Button disabled={isLoading} onClick={onBackButtonClick}>{t('Back')}</Button>
<Button primary disabled={isLoading || !cloudKey.trim()} marginInlineStart='auto' onClick={handleFinishButtonClick}>
{isLoading ? <Throbber inheritColor /> : t('Finish Registration')}
{isLoading ? <Throbber inheritColor /> : t('Finish_Registration')}
</Button>
</ButtonGroup>
</Modal.Footer>

@ -10,13 +10,17 @@ import { useEndpointAction } from '../../../hooks/useEndpointAction';
import VerticalBar from '../../../components/VerticalBar';
import DeleteSuccessModal from '../../../components/DeleteSuccessModal';
import DeleteWarningModal from '../../../components/DeleteWarningModal';
import { EmojiDescriptor } from './types';
import { useAbsoluteUrl } from '../../../contexts/ServerContext';
type EditCustomEmojiProps = {
close: () => void;
onChange: () => void;
data: EmojiDescriptor;
data: {
_id: string;
name: string;
aliases: string[];
extension: string;
};
};
const EditCustomEmoji: FC<EditCustomEmojiProps> = ({ close, onChange, data, ...props }) => {

@ -3,7 +3,6 @@ import { Box, Button, ButtonGroup, Skeleton, Throbber, InputBox } from '@rocket.
import { useTranslation } from '../../../contexts/TranslationContext';
import EditCustomEmoji from './EditCustomEmoji';
import { EmojiDescriptor } from './types';
import { useEndpointData } from '../../../hooks/useEndpointData';
import { AsyncStatePhase } from '../../../hooks/useAsyncState';
@ -26,11 +25,7 @@ const EditCustomEmojiWithData: FC<EditCustomEmojiWithDataProps> = ({ _id, onChan
phase: state,
error,
reload,
} = useEndpointData<{
emojis?: {
update: EmojiDescriptor[];
};
}>('emoji-custom.list', query);
} = useEndpointData('emoji-custom.list', query);
if (state === AsyncStatePhase.LOADING) {
return <Box pb='x20'>

@ -1,6 +0,0 @@
export type EmojiDescriptor = {
_id: string;
name: string;
aliases: string[];
extension: string;
};

@ -16,9 +16,7 @@ export const EditCustomUserStatusWithData: FC<EditCustomUserStatusWithDataProps>
const t = useTranslation();
const query = useMemo(() => ({ query: JSON.stringify({ _id }) }), [_id]);
const { value: data, phase: state, error, reload } = useEndpointData<{
statuses: unknown[];
}>('custom-user-status.list', query);
const { value: data, phase: state, error, reload } = useEndpointData('custom-user-status.list', query);
if (state === AsyncStatePhase.LOADING) {
return <Box pb='x20'>

@ -11,7 +11,7 @@ import { useRoute } from '../../../contexts/RouterContext';
const style = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' };
export const DEFAULT_TYPES = ['d', 'p', 'c'];
export const DEFAULT_TYPES = ['d', 'p', 'c', 'teams'];
export const roomTypeI18nMap = {
l: 'Omnichannel',
@ -19,11 +19,12 @@ export const roomTypeI18nMap = {
d: 'Direct',
p: 'Group',
discussion: 'Discussion',
team: 'Team',
};
const FilterByTypeAndText = ({ setFilter, ...props }) => {
const [text, setText] = useState('');
const [types, setTypes] = useState({ d: false, c: false, p: false, l: false, discussions: false });
const [types, setTypes] = useState({ d: false, c: false, p: false, l: false, discussions: false, teams: false });
const t = useTranslation();
@ -43,6 +44,7 @@ const FilterByTypeAndText = ({ setFilter, ...props }) => {
const idPrivate = useUniqueId();
const idOmnichannel = useUniqueId();
const idDiscussions = useUniqueId();
const idTeam = useUniqueId();
return <Box mb='x16' is='form' onSubmit={useCallback((e) => e.preventDefault(), [])} display='flex' flexDirection='column' {...props}>
<TextInput flexShrink={0} placeholder={t('Search_Rooms')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} />
@ -69,6 +71,10 @@ const FilterByTypeAndText = ({ setFilter, ...props }) => {
<CheckBox checked={types.discussions} id={idDiscussions} onChange={() => handleCheckBox('discussions')}/>
<Field.Label htmlFor={idDiscussions}>{t('Discussions')}</Field.Label>
</Field.Row>
<Field.Row>
<CheckBox checked={types.teams} id={idTeam} onChange={() => handleCheckBox('teams')}/>
<Field.Label htmlFor={idTeam}>{t('Teams')}</Field.Label>
</Field.Row>
</Margins>
</Box>
</Field>

@ -4,7 +4,7 @@ import React, { useCallback, useState, useMemo, FC } from 'react';
import { ISetting } from '../../../../definition/ISetting';
import { useSettings } from '../../../contexts/SettingsContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { TranslationKey, useTranslation } from '../../../contexts/TranslationContext';
import Sidebar from '../../../components/Sidebar';
const useSettingsGroups = (filter: string): ISetting[] => {
@ -18,8 +18,8 @@ const useSettingsGroups = (filter: string): ISetting[] => {
}
const getMatchableStrings = (setting: ISetting): string[] => [
setting.i18nLabel && t(setting.i18nLabel),
t(setting._id),
setting.i18nLabel && t(setting.i18nLabel as TranslationKey),
t(setting._id as TranslationKey),
setting._id,
].filter(Boolean);
@ -48,7 +48,7 @@ const useSettingsGroups = (filter: string): ISetting[] => {
return settings
.filter(({ type, group, _id }) => type === 'group' && groupIds.includes(group || _id))
.sort((a, b) => t(a.i18nLabel || a._id).localeCompare(t(b.i18nLabel || b._id)));
.sort((a, b) => t((a.i18nLabel || a._id) as TranslationKey).localeCompare(t((b.i18nLabel || b._id) as TranslationKey)));
}, [settings, filterPredicate, t]);
};
@ -78,7 +78,7 @@ const AdminSidebarSettings: FC<AdminSidebarSettingsProps> = ({ currentPath }) =>
{isLoadingGroups && <Skeleton/>}
{!isLoadingGroups && !!groups.length && <Sidebar.ItemsAssembler
items={groups.map((group) => ({
name: t(group.i18nLabel || group._id),
name: t((group.i18nLabel || group._id) as TranslationKey),
pathSection: 'admin',
pathGroup: group._id,
}))}

@ -1,7 +1,7 @@
import { ButtonGroup, Menu, Option } from '@rocket.chat/fuselage';
import React, { useCallback, useMemo } from 'react';
import { useUserInfoActionsSpread } from '../../room/hooks/useUserInfoActions';
import { useActionSpread } from '../../hooks/useActionSpread';
import ConfirmOwnerChangeWarningModal from '../../../components/ConfirmOwnerChangeWarningModal';
import { UserInfo } from '../../room/contextualBar/UserInfo';
import { usePermission } from '../../../contexts/AuthorizationContext';
@ -234,7 +234,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
username,
]);
const { actions: actionsDefinition, menu: menuOptions } = useUserInfoActionsSpread(options);
const { actions: actionsDefinition, menu: menuOptions } = useActionSpread(options);
const menu = useMemo(() => {
if (!menuOptions) {

@ -5,6 +5,7 @@ import Page from '../../components/Page';
import { useTranslation } from '../../contexts/TranslationContext';
import UserTab from './UserTab';
import ChannelsTab from './ChannelsTab';
import TeamsTab from './TeamsTab';
import { useRoute, useRouteParameter } from '../../contexts/RouterContext';
import { useSetting } from '../../contexts/SettingsContext';
@ -31,12 +32,14 @@ function DirectoryPage() {
<Tabs flexShrink={0} >
<Tabs.Item selected={tab === 'channels'} onClick={handleTabClick('channels')}>{t('Channels')}</Tabs.Item>
<Tabs.Item selected={tab === 'users'} onClick={handleTabClick('users')}>{t('Users')}</Tabs.Item>
<Tabs.Item selected={tab === 'teams'} onClick={handleTabClick('teams')}>{t('Teams')}</Tabs.Item>
{ federationEnabled && <Tabs.Item selected={tab === 'external'} onClick={handleTabClick('external')}>{t('External_Users')}</Tabs.Item> }
</Tabs>
<Page.Content>
{
(tab === 'users' && <UserTab />)
|| (tab === 'channels' && <ChannelsTab />)
|| (tab === 'teams' && < TeamsTab/>)
|| (federationEnabled && tab === 'external' && <UserTab workspace='external' />)
}

@ -0,0 +1,109 @@
import { Box, Margins, Table, Avatar, Tag } from '@rocket.chat/fuselage';
import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
import React, { useMemo, useState, useCallback } from 'react';
import GenericTable from '../../components/GenericTable';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import { usePermission } from '../../contexts/AuthorizationContext';
import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useEndpointData } from '../../hooks/useEndpointData';
import { useFormatDate } from '../../hooks/useFormatDate';
import { roomTypes } from '../../../app/utils/client';
import { useQuery } from './hooks';
const style = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' };
function RoomTags({ room }) {
const t = useTranslation();
return <Box mi='x4' alignItems='center' display='flex'>
<Margins inline='x2'>
{room.default && <Tag variant='primary'>{t('default')}</Tag>}
{room.featured && <Tag variant='primary'>{t('featured')}</Tag>}
</Margins>
</Box>;
}
function TeamsTable() {
const t = useTranslation();
const [sort, setSort] = useState(['name', 'asc']);
const [params, setParams] = useState({ current: 0, itemsPerPage: 25 });
const mediaQuery = useMediaQuery('(min-width: 768px)');
const onHeaderClick = useCallback((id) => {
const [sortBy, sortDirection] = sort;
if (sortBy === id) {
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
return;
}
setSort([id, 'asc']);
}, [sort]);
const header = useMemo(() => [
<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name'>{t('Name')}</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'channelsCount'} direction={sort[1]} active={sort[0] === 'channelsCount'} onClick={onHeaderClick} sort='channelsCount' style={{ width: '100px' }}>{t('Channels')}</GenericTable.HeaderCell>,
mediaQuery && <GenericTable.HeaderCell key={'createdAt'} direction={sort[1]} active={sort[0] === 'createdAt'} onClick={onHeaderClick} sort='createdAt' style={{ width: '150px' }}>{t('Created_at')}</GenericTable.HeaderCell>,
].filter(Boolean), [sort, onHeaderClick, t, mediaQuery]);
const channelsRoute = useRoute('channel');
const groupsRoute = useRoute('group');
const query = useQuery(params, sort);
const { value: data = { result: [] } } = useEndpointData('teams.list', query);
const onClick = useMemo(() => (name, type) => (e) => {
if (e.type === 'click' || e.key === 'Enter') {
type === 0 ? channelsRoute.push({ name }) : groupsRoute.push({ name });
}
}, [channelsRoute, groupsRoute]);
const formatDate = useFormatDate();
const renderRow = useCallback((team) => {
const { _id, createdAt, name, type, rooms, roomId } = team;
const t = type === 0 ? 'c' : 'p';
const avatarUrl = roomTypes.getConfig(t).getAvatarPath({ _id: roomId });
return <Table.Row key={_id} onKeyDown={onClick(name, type)} onClick={onClick(name, type)} tabIndex={0} role='link' action>
<Table.Cell>
<Box display='flex'>
<Box flexGrow={0}>
<Avatar size='x40' title={name} url={avatarUrl} />
</Box>
<Box grow={1} mi='x8' style={style}>
<Box display='flex' alignItems='center'>
<Box fontScale='p2' mi='x4'>{name}</Box><RoomTags room={team} style={style} />
</Box>
</Box>
</Box>
</Table.Cell>
<Table.Cell fontScale='p1' color='hint' style={style}>
{rooms}
</Table.Cell>
{ mediaQuery && <Table.Cell fontScale='p1' color='hint' style={style}>
{formatDate(createdAt)}
</Table.Cell>}
</Table.Row>;
}
, [formatDate, mediaQuery, onClick]);
return <GenericTable
header={header}
renderRow={renderRow}
results={data.teams}
setParams={setParams}
total={data.total}
/>;
}
export default function TeamsTab(props) {
const canViewPublicRooms = usePermission('view-c-room');
if (canViewPublicRooms) {
return <TeamsTable {...props} />;
}
return <NotAuthorizedPage />;
}

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

Loading…
Cancel
Save