[Chore] roomTypes: Stop mixing client and server code together

pull/24536/head
Pierre Lehnen 4 years ago
parent d02ea1daf7
commit 0545ced64f
  1. 50
      app/api/server/lib/rooms.js
  2. 13
      app/discussion/lib/discussionRoomType.js
  3. 4
      app/invites/server/functions/validateInviteToken.js
  4. 143
      app/lib/lib/roomTypes/direct.js
  5. 4
      app/lib/server/functions/notifications/email.js
  6. 190
      app/utils/lib/RoomTypeConfig.js
  7. 40
      app/utils/lib/roomExit.js
  8. 67
      client/lib/rooms/roomCoordinator.ts
  9. 33
      client/lib/rooms/roomExit.ts
  10. 135
      client/lib/rooms/roomTypes/direct.ts
  11. 8
      definition/IRoom.ts
  12. 100
      definition/IRoomTypeConfig.ts
  13. 2
      definition/externals/meteor/kadira-flow-router.d.ts
  14. 2
      definition/utils.ts
  15. 29
      lib/rooms/adminFields.ts
  16. 109
      lib/rooms/coordinator.ts
  17. 10
      lib/rooms/roomTypes/conversation.ts
  18. 22
      lib/rooms/roomTypes/direct.ts
  19. 10
      lib/rooms/roomTypes/discussion.ts
  20. 12
      lib/rooms/roomTypes/favorite.ts
  21. 18
      lib/rooms/roomTypes/private.ts
  22. 18
      lib/rooms/roomTypes/public.ts
  23. 10
      lib/rooms/roomTypes/unread.ts
  24. 82
      server/lib/rooms/roomCoordinator.ts
  25. 75
      server/lib/rooms/roomTypes/direct.ts

@ -1,34 +1,12 @@
import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { Rooms } from '../../../models/server/raw';
import { Subscriptions } from '../../../models/server';
import { adminFields } from '../../../../lib/rooms/adminFields';
export async function findAdminRooms({ uid, filter, types = [], pagination: { offset, count, sort } }) {
if (!(await hasPermissionAsync(uid, 'view-room-administration'))) {
throw new Error('error-not-authorized');
}
const fields = {
prid: 1,
fname: 1,
name: 1,
t: 1,
cl: 1,
u: 1,
usernames: 1,
usersCount: 1,
muted: 1,
unmuted: 1,
ro: 1,
default: 1,
favorite: 1,
featured: 1,
topic: 1,
msgs: 1,
archived: 1,
tokenpass: 1,
teamId: 1,
teamMain: 1,
};
const name = filter && filter.trim();
const discussion = types && types.includes('discussions');
const includeTeams = types && types.includes('teams');
@ -36,7 +14,7 @@ export async function findAdminRooms({ uid, filter, types = [], pagination: { of
const typesToRemove = ['discussions', 'teams'];
const showTypes = Array.isArray(types) ? types.filter((type) => !typesToRemove.includes(type)) : [];
const options = {
fields,
fields: adminFields,
sort: sort || { default: -1, name: 1 },
skip: offset,
limit: count,
@ -67,30 +45,8 @@ export async function findAdminRoom({ uid, rid }) {
if (!(await hasPermissionAsync(uid, 'view-room-administration'))) {
throw new Error('error-not-authorized');
}
const fields = {
prid: 1,
fname: 1,
name: 1,
t: 1,
cl: 1,
u: 1,
usernames: 1,
usersCount: 1,
muted: 1,
unmuted: 1,
ro: 1,
default: 1,
favorite: 1,
featured: 1,
topic: 1,
msgs: 1,
archived: 1,
tokenpass: 1,
announcement: 1,
description: 1,
};
return Rooms.findOneById(rid, { fields });
return Rooms.findOneById(rid, { fields: adminFields });
}
export async function findChannelAndPrivateAutocomplete({ uid, selector }) {

@ -1,13 +0,0 @@
import { RoomTypeConfig, roomTypes } from '../../utils';
export class DiscussionRoomType extends RoomTypeConfig {
constructor() {
super({
identifier: 't',
order: 25,
label: 'Discussion',
});
}
}
roomTypes.add(new DiscussionRoomType());

@ -19,9 +19,7 @@ export const validateInviteToken = async (token) => {
});
}
const room = Rooms.findOneById(inviteData.rid, {
fields: { _id: 1, name: 1, fname: 1, t: 1, prid: 1 },
});
const room = Rooms.findOneById(inviteData.rid);
if (!room) {
throw new Meteor.Error('error-invalid-room', 'The invite token is invalid.', {
method: 'validateInviteToken',

@ -1,13 +1,10 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { ChatRoom, Subscriptions } from '../../../models';
import { openRoom } from '../../../ui-utils';
import { getUserPreference, RoomTypeConfig, RoomTypeRouteConfig, RoomSettingsEnum, RoomMemberActions, UiTextContext } from '../../../utils';
import { hasPermission, hasAtLeastOnePermission } from '../../../authorization';
import { RoomTypeConfig, RoomTypeRouteConfig } from '../../../utils';
import { hasPermission } from '../../../authorization';
import { settings } from '../../../settings';
import { getUserAvatarURL } from '../../../utils/lib/getUserAvatarURL';
import { getAvatarURL } from '../../../utils/lib/getAvatarURL';
export class DirectMessageRoomRoute extends RoomTypeRouteConfig {
constructor() {
@ -37,13 +34,6 @@ export class DirectMessageRoomType extends RoomTypeConfig {
});
}
getIcon(roomData) {
if (this.isGroupChat(roomData)) {
return 'balloon';
}
return this.icon;
}
findRoom(identifier) {
if (!hasPermission('view-d-room')) {
return null;
@ -60,23 +50,6 @@ export class DirectMessageRoomType extends RoomTypeConfig {
}
}
roomName(roomData) {
// this function can receive different types of data
// if it doesn't have fname and name properties, should be a Room object
// so, need to find the related subscription
const subscription = roomData && (roomData.fname || roomData.name) ? roomData : Subscriptions.findOne({ rid: roomData._id });
if (subscription === undefined) {
return;
}
if (settings.get('UI_Use_Real_Name') && subscription.fname) {
return subscription.fname;
}
return subscription.name;
}
secondaryRoomName(roomData) {
if (settings.get('UI_Use_Real_Name')) {
const subscription = Subscriptions.findOne({ rid: roomData._id }, { fields: { name: 1 } });
@ -84,20 +57,6 @@ export class DirectMessageRoomType extends RoomTypeConfig {
}
}
condition() {
const groupByType = getUserPreference(Meteor.userId(), 'sidebarGroupByType');
return groupByType && hasAtLeastOnePermission(['view-d-room', 'view-joined-room']);
}
getUserStatus(roomId) {
const subscription = Subscriptions.findOne({ rid: roomId });
if (subscription == null) {
return;
}
return Session.get(`user_${subscription.name}_status`);
}
getUserStatusText(roomId) {
const subscription = Subscriptions.findOne({ rid: roomId });
if (subscription == null) {
@ -107,33 +66,6 @@ export class DirectMessageRoomType extends RoomTypeConfig {
return Session.get(`user_${subscription.name}_status_text`);
}
allowRoomSettingChange(room, setting) {
switch (setting) {
case RoomSettingsEnum.TYPE:
case RoomSettingsEnum.NAME:
case RoomSettingsEnum.SYSTEM_MESSAGES:
case RoomSettingsEnum.DESCRIPTION:
case RoomSettingsEnum.READ_ONLY:
case RoomSettingsEnum.REACT_WHEN_READ_ONLY:
case RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE:
case RoomSettingsEnum.JOIN_CODE:
return false;
case RoomSettingsEnum.E2E:
return settings.get('E2E_Enable') === true;
default:
return true;
}
}
allowMemberAction(room, action) {
switch (action) {
case RoomMemberActions.BLOCK:
return !this.isGroupChat(room);
default:
return false;
}
}
enableMembersListProfile() {
return true;
}
@ -141,75 +73,4 @@ export class DirectMessageRoomType extends RoomTypeConfig {
userDetailShowAll(/* room */) {
return true;
}
getUiText(context) {
switch (context) {
case UiTextContext.HIDE_WARNING:
return 'Hide_Private_Warning';
case UiTextContext.LEAVE_WARNING:
return 'Leave_Private_Warning';
default:
return '';
}
}
/**
* Returns details to use on notifications
*
* @param {object} room
* @param {object} user
* @param {string} notificationMessage
* @return {object} Notification details
*/
getNotificationDetails(room, user, notificationMessage) {
if (!Meteor.isServer) {
return {};
}
if (this.isGroupChat(room)) {
return {
title: this.roomName(room),
text: `${(settings.get('UI_Use_Real_Name') && user.name) || user.username}: ${notificationMessage}`,
};
}
return {
title: (settings.get('UI_Use_Real_Name') && user.name) || user.username,
text: notificationMessage,
};
}
getAvatarPath(roomData, subData) {
if (!roomData && !subData) {
return '';
}
// if coming from sidenav search
if (roomData.name && roomData.avatarETag) {
return getUserAvatarURL(roomData.name, roomData.avatarETag);
}
if (this.isGroupChat(roomData)) {
return getAvatarURL({ username: roomData.uids.length + roomData.usernames.join() });
}
const sub = subData || Subscriptions.findOne({ rid: roomData._id }, { fields: { name: 1 } });
if (sub && sub.name) {
const user = Meteor.users.findOne({ username: sub.name }, { fields: { username: 1, avatarETag: 1 } });
return getUserAvatarURL(user?.username || sub.name, user?.avatarETag);
}
if (roomData) {
return getUserAvatarURL(roomData.name || this.roomName(roomData)); // rooms should have no name for direct messages...
}
}
includeInDashboard() {
return true;
}
isGroupChat(room) {
return room && room.uids && room.uids.length > 2;
}
}

@ -99,7 +99,9 @@ function getEmailContent({ message, user, room }) {
}
const getButtonUrl = (room, subscription, message) => {
const path = `${s.ltrim(roomTypes.getRelativePath(room.t, subscription), '/')}?msg=${message._id}`;
const basePath = roomTypes.getRelativePath(room.t, subscription).replace(Meteor.absoluteUrl(), '');
const path = `${s.ltrim(basePath, '/')}?msg=${message._id}`;
return getURL(path, {
full: true,
cloud: settings.get('Offline_Message_Use_DeepLink'),

@ -1,51 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
let Users;
let settings;
if (Meteor.isServer) {
({ settings } = require('../../settings/server'));
Users = require('../../models/server/models/Users').default;
} else {
({ settings } = require('../../settings/client'));
}
export const RoomSettingsEnum = {
TYPE: 'type',
NAME: 'roomName',
TOPIC: 'roomTopic',
ANNOUNCEMENT: 'roomAnnouncement',
DESCRIPTION: 'roomDescription',
READ_ONLY: 'readOnly',
REACT_WHEN_READ_ONLY: 'reactWhenReadOnly',
ARCHIVE_OR_UNARCHIVE: 'archiveOrUnarchive',
JOIN_CODE: 'joinCode',
BROADCAST: 'broadcast',
SYSTEM_MESSAGES: 'systemMessages',
E2E: 'encrypted',
};
export const RoomMemberActions = {
ARCHIVE: 'archive',
IGNORE: 'ignore',
BLOCK: 'block',
MUTE: 'mute',
SET_AS_OWNER: 'setAsOwner',
SET_AS_LEADER: 'setAsLeader',
SET_AS_MODERATOR: 'setAsModerator',
LEAVE: 'leave',
REMOVE_USER: 'removeUser',
JOIN: 'join',
INVITE: 'invite',
};
export const UiTextContext = {
CLOSE_WARNING: 'closeWarning',
HIDE_WARNING: 'hideWarning',
LEAVE_WARNING: 'leaveWarning',
NO_ROOMS_SUBSCRIBED: 'noRoomsSubscribed',
};
export class RoomTypeRouteConfig {
constructor({ name, path }) {
if (typeof name !== 'undefined' && (typeof name !== 'string' || name.length === 0)) {
@ -103,101 +57,10 @@ export class RoomTypeConfig {
this._route = route;
}
/**
* The room type's internal identifier.
*/
get identifier() {
return this._identifier;
}
/**
* The order of this room type for the display.
*/
get order() {
return this._order;
}
/**
* Sets the order of this room type for the display.
*
* @param {number} order the number value for the order
*/
set order(order) {
if (typeof order !== 'number') {
throw new Error('The order must be a number.');
}
this._order = order;
}
/**
* The icon class, css, to use as the visual aid.
*/
get icon() {
return this._icon;
}
/**
* The header name of this type.
*/
get header() {
return this._header;
}
/**
* The i18n label for this room type.
*/
get label() {
return this._label;
}
/**
* The route config for this room type.
*/
get route() {
return this._route;
}
allowRoomSettingChange(/* room, setting */) {
return true;
}
allowMemberAction(/* room, action */) {
return false;
}
/**
* Return a room's name
*
* @abstract
* @return {string} Room's name according to it's type
*/
roomName(/* room */) {
return '';
}
canBeCreated(hasPermission) {
if (!hasPermission && typeof hasPermission !== 'function') {
throw new Error('You MUST provide the "hasPermission" to canBeCreated function');
}
return Meteor.isServer ? hasPermission(Meteor.userId(), `create-${this._identifier}`) : hasPermission([`create-${this._identifier}`]);
}
canBeDeleted(hasPermission, room) {
if (!hasPermission && typeof hasPermission !== 'function') {
throw new Error('You MUST provide the "hasPermission" to canBeDeleted function');
}
return Meteor.isServer ? hasPermission(Meteor.userId(), `delete-${room.t}`, room._id) : hasPermission(`delete-${room.t}`, room._id);
}
supportMembersList(/* room */) {
return true;
}
isGroupChat() {
return false;
}
canAddUser(/* userId, room */) {
return false;
}
@ -210,10 +73,6 @@ export class RoomTypeConfig {
return true;
}
preventRenaming(/* room */) {
return false;
}
includeInRoomSearch() {
return false;
}
@ -232,51 +91,6 @@ export class RoomTypeConfig {
return '';
}
/**
* Returns the full object of message sender
* @param {string} senderId Sender's _id
* @return {object} Sender's object from db
*/
getMsgSender(senderId) {
if (Meteor.isServer && Users) {
return Users.findOneById(senderId);
}
return {};
}
/**
* Returns details to use on notifications
*
* @param {object} room
* @param {object} user
* @param {string} notificationMessage
* @return {object} Notification details
*/
getNotificationDetails(room, user, notificationMessage) {
if (!Meteor.isServer) {
return {};
}
const title = `#${this.roomName(room)}`;
const text = `${settings.get('UI_Use_Real_Name') ? user.name : user.username}: ${notificationMessage}`;
return { title, text };
}
/**
* Check if there is an user with the same id and loginToken
* @param {object} allowData
* @return {object} User's object from db
*/
canAccessUploadedFile(/* accessData */) {
return false;
}
getReadReceiptsExtraData(/* message */) {
return {};
}
getAvatarPath(/* roomData */) {
return '';
}
@ -284,8 +98,4 @@ export class RoomTypeConfig {
openCustomProfileTab() {
return false;
}
getDiscussionType() {
return 'p';
}
}

@ -1,40 +0,0 @@
import { Blaze } from 'meteor/blaze';
// import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { callbacks } from '../../../lib/callbacks';
const testIfPathAreEquals = (oldPath = '', newPath = '') => oldPath.replace(/"/g, '') === newPath;
export const roomExit = function () {
const oldRoute = FlowRouter.current();
Tracker.afterFlush(() => {
const context = FlowRouter.current();
if (
oldRoute &&
testIfPathAreEquals(
oldRoute.params.name || oldRoute.params.rid || oldRoute.params.id,
context.params.name || context.params.rid || context.params.id,
)
) {
return;
}
// 7370 - Close flex-tab when opening a room on mobile UI
if (window.matchMedia('(max-width: 500px)').matches) {
const flex = document.querySelector('.flex-tab');
if (flex) {
const templateData = Blaze.getData(flex);
templateData && templateData.tabBar && templateData.tabBar.close();
}
}
callbacks.run('roomExit');
// Session.set('lastOpenedRoom', Session.get('openedRoom'));
// Session.set('openedRoom', null);
// RoomManager.openedRoom = null;
});
if (typeof window.currentTracker !== 'undefined') {
window.currentTracker.stop();
}
};

@ -0,0 +1,67 @@
import type { RouteOptions } from 'meteor/kadira:flow-router';
import { openRoom } from '../../../app/ui-utils/client/lib/openRoom';
import type { IRoom } from '../../../definition/IRoom';
import type { IRoomTypeConfig, IRoomTypeClientDirectives } from '../../../definition/IRoomTypeConfig';
import { RoomSettingsEnum, RoomMemberActions, UiTextContext } from '../../../definition/IRoomTypeConfig';
import type { ValueOf } from '../../../definition/utils';
import { RoomCoordinator } from '../../../lib/rooms/coordinator';
import { roomExit } from './roomExit';
class RoomCoordinatorClient extends RoomCoordinator {
add(roomConfig: IRoomTypeConfig, directives: Partial<IRoomTypeClientDirectives>): void {
this.addRoomType(roomConfig, {
allowRoomSettingChange(_room: Partial<IRoom>, _setting: ValueOf<typeof RoomSettingsEnum>): boolean {
return true;
},
allowMemberAction(_room: Partial<IRoom>, _action: ValueOf<typeof RoomMemberActions>): boolean {
return false;
},
roomName(_room: Partial<IRoom>): string {
return '';
},
isGroupChat(_room: Partial<IRoom>): boolean {
return false;
},
openCustomProfileTab(_instance: any, _room: IRoom, _username: string): boolean {
return false;
},
getUiText(_context: ValueOf<typeof UiTextContext>): string {
return '';
},
condition(): boolean {
return true;
},
getAvatarPath(_room: Partial<IRoom> & { username?: IRoom['_id'] }): string {
return '';
},
getIcon(_room: Partial<IRoom>): string | undefined {
return this.config.icon;
},
getUserStatus(_roomId: string): string | undefined {
return undefined;
},
...directives,
config: roomConfig,
});
}
protected addRoute(path: string, routeConfig: RouteOptions): void {
super.addRoute(path, { ...routeConfig, triggersExit: [roomExit] });
}
getRoomDirectives(roomType: string): IRoomTypeClientDirectives | undefined {
return this.roomTypes[roomType]?.directives as IRoomTypeClientDirectives;
}
openRoom(type: string, name: string, render = true): void {
openRoom(type, name, render);
}
getIcon(room: Partial<IRoom>): string | undefined {
return room?.t && this.getRoomDirectives(room.t)?.getIcon(room);
}
}
export const roomCoordinator = new RoomCoordinatorClient();

@ -0,0 +1,33 @@
import { Blaze } from 'meteor/blaze';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Tracker } from 'meteor/tracker';
const testIfPathAreEquals = (oldPath = '', newPath = ''): boolean => oldPath.replace(/"/g, '') === newPath;
export const roomExit = function (_context: { params: Record<string, string>; queryParams: Record<string, string> }): void {
const oldRoute = FlowRouter.current();
Tracker.afterFlush(() => {
const context = FlowRouter.current();
if (
oldRoute &&
testIfPathAreEquals(
oldRoute.params.name || oldRoute.params.rid || oldRoute.params.id,
context.params.name || context.params.rid || context.params.id,
)
) {
return;
}
// 7370 - Close flex-tab when opening a room on mobile UI
if (window.matchMedia('(max-width: 500px)').matches) {
const flex = document.querySelector<HTMLElement>('.flex-tab');
if (flex) {
const templateData = Blaze.getData(flex) as any;
templateData?.tabBar?.close();
}
}
});
if (typeof (window as any).currentTracker !== 'undefined') {
(window as any).currentTracker.stop();
}
};

@ -0,0 +1,135 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { hasAtLeastOnePermission } from '../../../../app/authorization/client';
import { Subscriptions, Users } from '../../../../app/models/client';
import { settings } from '../../../../app/settings/client';
import { getUserPreference } from '../../../../app/utils/client';
import { getAvatarURL } from '../../../../app/utils/lib/getAvatarURL';
import { getUserAvatarURL } from '../../../../app/utils/lib/getUserAvatarURL';
import type { IRoom } from '../../../../definition/IRoom';
import type { IRoomTypeClientDirectives } from '../../../../definition/IRoomTypeConfig';
import { RoomSettingsEnum, RoomMemberActions, UiTextContext } from '../../../../definition/IRoomTypeConfig';
import type { AtLeast, ValueOf } from '../../../../definition/utils';
import { getDirectMessageRoomType } from '../../../../lib/rooms/roomTypes/direct';
import { roomCoordinator } from '../roomCoordinator';
export const DirectMessageRoomType = getDirectMessageRoomType(roomCoordinator);
roomCoordinator.add(DirectMessageRoomType, {
allowRoomSettingChange(_room: Partial<IRoom>, setting: ValueOf<typeof RoomSettingsEnum>): boolean {
switch (setting) {
case RoomSettingsEnum.TYPE:
case RoomSettingsEnum.NAME:
case RoomSettingsEnum.SYSTEM_MESSAGES:
case RoomSettingsEnum.DESCRIPTION:
case RoomSettingsEnum.READ_ONLY:
case RoomSettingsEnum.REACT_WHEN_READ_ONLY:
case RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE:
case RoomSettingsEnum.JOIN_CODE:
return false;
case RoomSettingsEnum.E2E:
return settings.get('E2E_Enable') === true;
default:
return true;
}
},
allowMemberAction(room: Partial<IRoom>, action: ValueOf<typeof RoomMemberActions>): boolean {
switch (action) {
case RoomMemberActions.BLOCK:
return !this.isGroupChat(room);
default:
return false;
}
},
roomName(roomData: Partial<IRoom>): string | undefined {
const subscription = ((): { fname?: string; name?: string } | undefined => {
if (roomData.fname || roomData.name) {
return {
fname: roomData.fname,
name: roomData.name,
};
}
if (!roomData._id) {
return undefined;
}
return Subscriptions.findOne({ rid: roomData._id });
})();
if (!subscription) {
return;
}
if (settings.get('UI_Use_Real_Name') && subscription.fname) {
return subscription.fname;
}
return subscription.name;
},
isGroupChat(room: Partial<IRoom>): boolean {
return (room?.uids?.length || 0) > 2;
},
getUiText(context: ValueOf<typeof UiTextContext>): string {
switch (context) {
case UiTextContext.HIDE_WARNING:
return 'Hide_Private_Warning';
case UiTextContext.LEAVE_WARNING:
return 'Leave_Private_Warning';
default:
return '';
}
},
condition(): boolean {
const groupByType = getUserPreference(Meteor.userId(), 'sidebarGroupByType');
return groupByType && hasAtLeastOnePermission(['view-d-room', 'view-joined-room']);
},
getAvatarPath(room: Partial<IRoom> & { username?: IRoom['_id'] }): string {
if (!room) {
return '';
}
// if coming from sidenav search
if (room.name && room.avatarETag) {
return getUserAvatarURL(room.name, room.avatarETag);
}
if (this.isGroupChat(room)) {
return getAvatarURL({
username: (room.uids || []).length + (room.usernames || []).join(),
roomId: undefined,
cache: room.avatarETag,
});
}
const sub = Subscriptions.findOne({ rid: room._id }, { fields: { name: 1 } });
if (sub?.name) {
const user = Users.findOne({ username: sub.name }, { fields: { username: 1, avatarETag: 1 } });
return getUserAvatarURL(user?.username || sub.name, user?.avatarETag);
}
return getUserAvatarURL(room.name || this.roomName(room));
},
getIcon(room: Partial<IRoom>): string | undefined {
if (this.isGroupChat(room)) {
return 'balloon';
}
},
getUserStatus(roomId: string): string | undefined {
const subscription = Subscriptions.findOne({ rid: roomId });
if (!subscription) {
return;
}
return Session.get(`user_${subscription.name}_status`);
},
} as AtLeast<IRoomTypeClientDirectives, 'isGroupChat' | 'roomName'>);

@ -67,9 +67,17 @@ export interface IRoom extends IRocketChatRecord {
sysMes?: string[];
muted?: string[];
unmuted?: string[];
usernames?: string[];
ts?: Date;
cl?: boolean;
ro?: boolean;
favorite?: boolean;
archived?: boolean;
announcement?: string;
description?: string;
}
export interface ICreatedRoom extends IRoom {

@ -0,0 +1,100 @@
import type { RouteOptions } from 'meteor/kadira:flow-router';
import type { IRoom, RoomType } from './IRoom';
import type { ISubscription } from './ISubscription';
import type { IRocketChatRecord } from './IRocketChatRecord';
import type { IUser } from './IUser';
import type { IMessage } from './IMessage';
import type { ReadReceipt } from './ReadReceipt';
import type { ValueOf, AtLeast } from './utils';
export type RoomData = IRoom | ISubscription | { name: IUser['username'] };
export interface IRoomTypeRouteConfig {
name: string;
path?: string;
action?: RouteOptions['action'];
link?: (data: RoomData) => Record<string, string>;
}
export const RoomSettingsEnum = {
TYPE: 'type',
NAME: 'roomName',
TOPIC: 'roomTopic',
ANNOUNCEMENT: 'roomAnnouncement',
DESCRIPTION: 'roomDescription',
READ_ONLY: 'readOnly',
REACT_WHEN_READ_ONLY: 'reactWhenReadOnly',
ARCHIVE_OR_UNARCHIVE: 'archiveOrUnarchive',
JOIN_CODE: 'joinCode',
BROADCAST: 'broadcast',
SYSTEM_MESSAGES: 'systemMessages',
E2E: 'encrypted',
} as const;
export const RoomMemberActions = {
ARCHIVE: 'archive',
IGNORE: 'ignore',
BLOCK: 'block',
MUTE: 'mute',
SET_AS_OWNER: 'setAsOwner',
SET_AS_LEADER: 'setAsLeader',
SET_AS_MODERATOR: 'setAsModerator',
LEAVE: 'leave',
REMOVE_USER: 'removeUser',
JOIN: 'join',
INVITE: 'invite',
} as const;
export const UiTextContext = {
CLOSE_WARNING: 'closeWarning',
HIDE_WARNING: 'hideWarning',
LEAVE_WARNING: 'leaveWarning',
NO_ROOMS_SUBSCRIBED: 'noRoomsSubscribed',
} as const;
export interface IRoomTypeConfig {
identifier: string;
order: number;
icon?: string;
header?: string;
label?: string;
route?: IRoomTypeRouteConfig;
}
export interface IRoomTypeClientDirectives {
config: IRoomTypeConfig;
allowRoomSettingChange: (room: Partial<IRoom>, setting: ValueOf<typeof RoomSettingsEnum>) => boolean;
allowMemberAction: (room: Partial<IRoom>, action: ValueOf<typeof RoomMemberActions>) => boolean;
roomName: (room: Partial<IRoom>) => string | undefined;
isGroupChat: (room: Partial<IRoom>) => boolean;
openCustomProfileTab: (instance: any, room: IRoom, username: string) => boolean;
getUiText: (context: ValueOf<typeof UiTextContext>) => string;
condition: () => boolean;
getAvatarPath: (room: Partial<IRoom> & { username?: IRoom['_id'] }) => string;
getIcon: (room: Partial<IRoom>) => string | undefined;
getUserStatus: (roomId: string) => string | undefined;
}
export interface IRoomTypeServerDirectives {
config: IRoomTypeConfig;
allowRoomSettingChange: (room: IRoom, setting: ValueOf<typeof RoomSettingsEnum>) => boolean;
allowMemberAction: (room: IRoom, action: ValueOf<typeof RoomMemberActions>) => boolean;
roomName: (room: IRoom) => string | undefined;
isGroupChat: (room: IRoom) => boolean;
canBeDeleted: (hasPermission: (permissionId: string, rid?: string) => boolean, room: IRoom) => boolean;
preventRenaming: () => boolean;
getDiscussionType: () => RoomType;
canAccessUploadedFile: (params: { rc_uid: string; rc_rid: string; rc_token: string }) => boolean;
getNotificationDetails: (
room: IRoom,
user: AtLeast<IUser, '_id' | 'name' | 'username'>,
notificationMessage: string,
) => { title: string | undefined; text: string };
getMsgSender: (senderId: IRocketChatRecord['_id']) => IRocketChatRecord | undefined;
includeInRoomSearch: () => boolean;
getReadReceiptsExtraData: (message: IMessage) => Partial<ReadReceipt>;
includeInDashboard: () => boolean;
}

@ -6,7 +6,7 @@ declare module 'meteor/kadira:flow-router' {
queryParams: Record<string, string>;
};
type RouteOptions = {
export type RouteOptions = {
name: string;
action?: (this: Route, params?: Record<string, string>, queryParams?: Record<string, string>) => void;
subscriptions?: (this: Route, params?: Record<string, string>, queryParams?: Record<string, string>) => void;

@ -10,3 +10,5 @@ export type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
// `T extends any` is a trick to apply a operator to each member of a union
export type KeyOfEach<T> = T extends any ? keyof T : never;
export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;

@ -0,0 +1,29 @@
import type { IRoom } from '../../definition/IRoom';
export const adminFields: Partial<Record<keyof IRoom, 1>> = {
prid: 1,
fname: 1,
name: 1,
t: 1,
cl: 1,
u: 1,
usernames: 1,
usersCount: 1,
muted: 1,
unmuted: 1,
ro: 1,
default: 1,
favorite: 1,
featured: 1,
topic: 1,
msgs: 1,
archived: 1,
tokenpass: 1,
teamId: 1,
teamMain: 1,
announcement: 1,
description: 1,
broadcast: 1,
uids: 1,
avatarETag: 1,
} as const;

@ -0,0 +1,109 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import type { RouteOptions } from 'meteor/kadira:flow-router';
import type { IRoomTypeConfig, IRoomTypeClientDirectives, IRoomTypeServerDirectives, RoomData } from '../../definition/IRoomTypeConfig';
import type { IRoom } from '../../definition/IRoom';
import type { ISubscription } from '../../definition/ISubscription';
import type { SettingValue } from '../../definition/ISetting';
export abstract class RoomCoordinator {
roomTypes: Record<string, { config: IRoomTypeConfig; directives: IRoomTypeClientDirectives | IRoomTypeServerDirectives }>;
roomTypesOrder: Array<{ identifier: string; order: number }>;
mainOrder: number;
constructor() {
this.roomTypes = {};
this.roomTypesOrder = [];
this.mainOrder = 1;
}
protected addRoomType(roomConfig: IRoomTypeConfig, directives: IRoomTypeClientDirectives | IRoomTypeServerDirectives): void {
if (this.roomTypes[roomConfig.identifier]) {
return;
}
if (!roomConfig.order) {
roomConfig.order = this.mainOrder + 10;
this.mainOrder += 10;
}
this.roomTypesOrder.push({
identifier: roomConfig.identifier,
order: roomConfig.order,
});
this.roomTypes[roomConfig.identifier] = { config: roomConfig, directives };
if (roomConfig.route && roomConfig.route.path && roomConfig.route.name && roomConfig.route.action) {
const routeConfig = {
name: roomConfig.route.name,
action: roomConfig.route.action,
};
return this.addRoute(roomConfig.route.path, routeConfig);
}
}
protected addRoute(path: string, routeConfig: RouteOptions): void {
FlowRouter.route(path, routeConfig);
}
getSetting(_settingId: string): SettingValue {
return undefined;
}
getRoomTypeConfig(identifier: string): IRoomTypeConfig | undefined {
return this.roomTypes[identifier]?.config;
}
getRouteLink(roomType: string, subData: RoomData): string | false {
const config = this.getRoomTypeConfig(roomType);
if (!config?.route) {
return false;
}
const routeData = this.getRouteData(roomType, subData);
if (!routeData) {
return false;
}
return FlowRouter.path(config.route.name, routeData);
}
getURL(roomType: string, subData: IRoom | ISubscription): string | false {
const config = this.getRoomTypeConfig(roomType);
if (!config?.route) {
return false;
}
const routeData = this.getRouteData(roomType, subData);
if (!routeData) {
return false;
}
return FlowRouter.url(config.route.name, routeData);
}
getRouteData(roomType: string, subData: RoomData): Record<string, string> | false {
const config = this.getRoomTypeConfig(roomType);
if (!config) {
return false;
}
let routeData = {};
if (config.route?.link) {
routeData = config.route.link(subData);
} else if (subData?.name) {
routeData = {
rid: (subData as ISubscription).rid || (subData as IRoom)._id,
name: subData.name,
};
}
return routeData;
}
abstract openRoom(_type: string, _name: string, _render?: boolean): void;
}

@ -0,0 +1,10 @@
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
import type { RoomCoordinator } from '../coordinator';
export function getConversationRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
return {
identifier: 'merged',
order: 30,
label: 'Conversations',
};
}

@ -0,0 +1,22 @@
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
import type { RoomCoordinator } from '../coordinator';
import type { ISubscription } from '../../../definition/ISubscription';
export function getDirectMessageRoomType(coordinator: RoomCoordinator): IRoomTypeConfig {
return {
identifier: 'd',
order: 50,
icon: 'at',
label: 'Direct_Messages',
route: {
name: 'direct',
path: '/direct/:rid/:tab?/:context?',
action: ({ rid } = {}): void => {
return coordinator.openRoom('d', rid);
},
link(sub): Record<string, string> {
return { rid: (sub as ISubscription).rid || sub.name || '' };
},
},
};
}

@ -0,0 +1,10 @@
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
import type { RoomCoordinator } from '../coordinator';
export function getDiscussionRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
return {
identifier: 't',
order: 25,
label: 'Discussion',
};
}

@ -0,0 +1,12 @@
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
import type { RoomCoordinator } from '../coordinator';
export function getFavoriteRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
return {
identifier: 'f',
order: 20,
header: 'favorite',
icon: 'star',
label: 'Favorites',
};
}

@ -0,0 +1,18 @@
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
import type { RoomCoordinator } from '../coordinator';
export function getPrivateRoomType(coordinator: RoomCoordinator): IRoomTypeConfig {
return {
identifier: 'p',
order: 40,
icon: 'hashtag-lock',
label: 'Private_Groups',
route: {
name: 'group',
path: '/group/:name/:tab?/:context?',
action: ({ name } = {}): void => {
return coordinator.openRoom('p', name);
},
},
};
}

@ -0,0 +1,18 @@
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
import type { RoomCoordinator } from '../coordinator';
export function getPublicRoomType(coordinator: RoomCoordinator): IRoomTypeConfig {
return {
identifier: 'c',
order: 30,
icon: 'hashtag',
label: 'Channels',
route: {
name: 'channel',
path: '/channel/:name/:tab?/:context?',
action: ({ name } = {}): void => {
return coordinator.openRoom('c', name);
},
},
};
}

@ -0,0 +1,10 @@
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
import type { RoomCoordinator } from '../coordinator';
export function getUnreadRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
return {
identifier: 'unread',
order: 10,
label: 'Unread',
};
}

@ -0,0 +1,82 @@
import type { IRoomTypeConfig, IRoomTypeServerDirectives } from '../../../definition/IRoomTypeConfig';
import type { IRoom, RoomType } from '../../../definition/IRoom';
import type { IUser } from '../../../definition/IUser';
import type { IMessage } from '../../../definition/IMessage';
import type { ReadReceipt } from '../../../definition/ReadReceipt';
import type { IRocketChatRecord } from '../../../definition/IRocketChatRecord';
import type { ValueOf, AtLeast } from '../../../definition/utils';
import { Users } from '../../../app/models/server';
import { RoomSettingsEnum, RoomMemberActions } from '../../../definition/IRoomTypeConfig';
import { RoomCoordinator } from '../../../lib/rooms/coordinator';
import { settings } from '../../../app/settings/server';
class RoomCoordinatorServer extends RoomCoordinator {
add(roomConfig: IRoomTypeConfig, directives: Partial<IRoomTypeServerDirectives>): void {
this.addRoomType(roomConfig, {
allowRoomSettingChange(_room: IRoom, _setting: ValueOf<typeof RoomSettingsEnum>): boolean {
return true;
},
allowMemberAction(_room: IRoom, _action: ValueOf<typeof RoomMemberActions>): boolean {
return false;
},
roomName(_room: IRoom): string {
return '';
},
isGroupChat(_room: IRoom): boolean {
return false;
},
canBeDeleted(hasPermission: (permissionId: string, rid?: string) => boolean, room: IRoom): boolean {
if (!hasPermission && typeof hasPermission !== 'function') {
throw new Error('You MUST provide the "hasPermission" to canBeDeleted function');
}
return hasPermission(`delete-${room.t}`, room._id);
},
preventRenaming(): boolean {
return false;
},
getDiscussionType(): RoomType {
return 'p';
},
canAccessUploadedFile(_params: { rc_uid: string; rc_rid: string; rc_token: string }): boolean {
return false;
},
getNotificationDetails(
room: IRoom,
user: AtLeast<IUser, '_id' | 'name' | 'username'>,
notificationMessage: string,
): { title: string | undefined; text: string } {
const title = `#${this.roomName(room)}`;
const name = settings.get<boolean>('UI_Use_Real_Name') ? user.name : user.username;
const text = `${name}: ${notificationMessage}`;
return { title, text };
},
getMsgSender(senderId: IRocketChatRecord['_id']): IRocketChatRecord | undefined {
return Users.findOneById(senderId);
},
includeInRoomSearch(): boolean {
return false;
},
getReadReceiptsExtraData(_message: IMessage): Partial<ReadReceipt> {
return {};
},
includeInDashboard(): boolean {
return false;
},
...directives,
config: roomConfig,
});
}
getRoomDirectives(roomType: string): IRoomTypeServerDirectives | undefined {
return this.roomTypes[roomType]?.directives as IRoomTypeServerDirectives;
}
openRoom(_type: string, _name: string, _render = true): void {
// Nothing to do on the server side.
}
}
export const roomCoordinator = new RoomCoordinatorServer();

@ -0,0 +1,75 @@
import { settings } from '../../../../app/settings/server';
import type { IRoom } from '../../../../definition/IRoom';
import type { IUser } from '../../../../definition/IUser';
import type { IRoomTypeServerDirectives } from '../../../../definition/IRoomTypeConfig';
import { RoomSettingsEnum, RoomMemberActions } from '../../../../definition/IRoomTypeConfig';
import type { AtLeast, ValueOf } from '../../../../definition/utils';
import { getDirectMessageRoomType } from '../../../../lib/rooms/roomTypes/direct';
import { roomCoordinator } from '../roomCoordinator';
export const DirectMessageRoomType = getDirectMessageRoomType(roomCoordinator);
roomCoordinator.add(DirectMessageRoomType, {
allowRoomSettingChange(_room: IRoom, setting: ValueOf<typeof RoomSettingsEnum>): boolean {
switch (setting) {
case RoomSettingsEnum.TYPE:
case RoomSettingsEnum.NAME:
case RoomSettingsEnum.SYSTEM_MESSAGES:
case RoomSettingsEnum.DESCRIPTION:
case RoomSettingsEnum.READ_ONLY:
case RoomSettingsEnum.REACT_WHEN_READ_ONLY:
case RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE:
case RoomSettingsEnum.JOIN_CODE:
return false;
case RoomSettingsEnum.E2E:
return settings.get('E2E_Enable') === true;
default:
return true;
}
},
allowMemberAction(room: IRoom, action: ValueOf<typeof RoomMemberActions>): boolean {
switch (action) {
case RoomMemberActions.BLOCK:
return !this.isGroupChat(room);
default:
return false;
}
},
roomName(room: IRoom): string | undefined {
if (settings.get('UI_Use_Real_Name') && room.fname) {
return room.fname;
}
return room.name;
},
isGroupChat(room: IRoom): boolean {
return (room?.uids?.length || 0) > 2;
},
getNotificationDetails(
room: IRoom,
user: AtLeast<IUser, '_id' | 'name' | 'username'>,
notificationMessage: string,
): { title: string | undefined; text: string } {
const useRealName = settings.get<boolean>('UI_Use_Real_Name');
if (!this.isGroupChat(room)) {
return {
title: this.roomName(room),
text: `${(useRealName && user.name) || user.username}: ${notificationMessage}`,
};
}
return {
title: (useRealName && user.name) || user.username,
text: notificationMessage,
};
},
includeInDashboard(): boolean {
return true;
},
} as AtLeast<IRoomTypeServerDirectives, 'isGroupChat' | 'roomName'>);
Loading…
Cancel
Save