diff --git a/app/constants/messageTypeLoad.ts b/app/constants/messageTypeLoad.ts index 4bdaa54de..6836981aa 100644 --- a/app/constants/messageTypeLoad.ts +++ b/app/constants/messageTypeLoad.ts @@ -1,5 +1,7 @@ -export const MESSAGE_TYPE_LOAD_MORE = 'load_more'; -export const MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK = 'load_previous_chunk'; -export const MESSAGE_TYPE_LOAD_NEXT_CHUNK = 'load_next_chunk'; +export enum MessageTypeLoad { + MORE = 'load_more', + PREVIOUS_CHUNK = 'load_previous_chunk', + NEXT_CHUNK = 'load_next_chunk' +} -export const MESSAGE_TYPE_ANY_LOAD = [MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, MESSAGE_TYPE_LOAD_NEXT_CHUNK]; +export const MESSAGE_TYPE_ANY_LOAD = [MessageTypeLoad.MORE, MessageTypeLoad.PREVIOUS_CHUNK, MessageTypeLoad.NEXT_CHUNK]; diff --git a/app/containers/Avatar/interfaces.ts b/app/containers/Avatar/interfaces.ts index ddec5b276..3bc5dd85e 100644 --- a/app/containers/Avatar/interfaces.ts +++ b/app/containers/Avatar/interfaces.ts @@ -1,3 +1,5 @@ +import React from 'react'; + import { TGetCustomEmoji } from '../../definitions/IEmoji'; export interface IAvatar { @@ -9,7 +11,7 @@ export interface IAvatar { size?: number; borderRadius?: number; type?: string; - children?: JSX.Element; + children?: React.ReactElement | null; user?: { id?: string; token?: string; diff --git a/app/containers/MessageBox/index.tsx b/app/containers/MessageBox/index.tsx index 34ca4eade..2ec4101de 100644 --- a/app/containers/MessageBox/index.tsx +++ b/app/containers/MessageBox/index.tsx @@ -922,8 +922,8 @@ class MessageBox extends Component { let msg = `[ ](${permalink}) `; // if original message wasn't sent by current user and neither from a direct room - if (user.username !== replyingMessage.u.username && roomType !== 'd' && replyWithMention) { - msg += `@${replyingMessage.u.username} `; + if (user.username !== replyingMessage?.u?.username && roomType !== 'd' && replyWithMention) { + msg += `@${replyingMessage?.u?.username} `; } msg = `${msg} ${message}`; diff --git a/app/definitions/IDDPMessage.ts b/app/definitions/IDDPMessage.ts new file mode 100644 index 000000000..7b6ad822a --- /dev/null +++ b/app/definitions/IDDPMessage.ts @@ -0,0 +1,7 @@ +export interface IDDPMessage { + msg: string; + fields: { + eventName: string; + args: any; + }; +} diff --git a/app/definitions/ILivechatVisitor.ts b/app/definitions/ILivechatVisitor.ts index 847d3b26c..8a942909a 100644 --- a/app/definitions/ILivechatVisitor.ts +++ b/app/definitions/ILivechatVisitor.ts @@ -32,6 +32,8 @@ export interface ILivechatVisitor extends IRocketChatRecord { ip?: string; host?: string; visitorEmails?: IVisitorEmail[]; + livechatData?: any; + utc?: number; } export interface ILivechatVisitorDTO { diff --git a/app/definitions/IMessage.ts b/app/definitions/IMessage.ts index 0d0a4683f..c0dbe4070 100644 --- a/app/definitions/IMessage.ts +++ b/app/definitions/IMessage.ts @@ -1,27 +1,14 @@ import Model from '@nozbe/watermelondb/Model'; import { MarkdownAST } from '@rocket.chat/message-parser'; +import { MessageTypeLoad } from '../constants/messageTypeLoad'; import { IAttachment } from './IAttachment'; import { IReaction } from './IReaction'; -import { - MESSAGE_TYPE_LOAD_MORE, - MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, - MESSAGE_TYPE_LOAD_NEXT_CHUNK -} from '../constants/messageTypeLoad'; import { TThreadMessageModel } from './IThreadMessage'; import { TThreadModel } from './IThread'; import { IUrlFromServer } from './IUrl'; -export type MessageType = - | 'jitsi_call_started' - | 'discussion-created' - | 'e2e' - | 'load_more' - | 'rm' - | 'uj' - | typeof MESSAGE_TYPE_LOAD_MORE - | typeof MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK - | typeof MESSAGE_TYPE_LOAD_NEXT_CHUNK; +export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj' | MessageTypeLoad; export interface IUserMessage { _id: string; diff --git a/app/definitions/IRoom.ts b/app/definitions/IRoom.ts index ed215e5f9..f63445a5e 100644 --- a/app/definitions/IRoom.ts +++ b/app/definitions/IRoom.ts @@ -15,7 +15,7 @@ interface IRequestTranscript { } export interface IRoom { - _id?: string; + _id: string; fname?: string; id: string; rid: string; diff --git a/app/definitions/ISubscription.ts b/app/definitions/ISubscription.ts index a0dd4c5a2..72e0c9a5e 100644 --- a/app/definitions/ISubscription.ts +++ b/app/definitions/ISubscription.ts @@ -30,13 +30,15 @@ export enum ERoomTypes { CHANNEL = 'channel' } +type RelationModified = { fetch(): Promise } & Relation; + export interface ISubscription { _id: string; // _id belongs watermelonDB id: string; // id from server _updatedAt?: string; // from server v?: IVisitor; f: boolean; - t: string; // TODO: we need to review this type later + t: SubscriptionType; // TODO: we need to review this type later ts: string | Date; ls: Date; name: string; @@ -91,12 +93,13 @@ export interface ISubscription { avatarETag?: string; teamId?: string; teamMain?: boolean; + unsubscribe: () => Promise; separator?: boolean; // https://nozbe.github.io/WatermelonDB/Relation.html#relation-api - messages: Relation; - threads: Relation; - threadMessages: Relation; - uploads: Relation; + messages: RelationModified; + threads: RelationModified; + threadMessages: RelationModified; + uploads: RelationModified; } export type TSubscriptionModel = ISubscription & Model; diff --git a/app/definitions/ITeam.ts b/app/definitions/ITeam.ts index 3b1774054..ae5f9544c 100644 --- a/app/definitions/ITeam.ts +++ b/app/definitions/ITeam.ts @@ -1,5 +1,6 @@ import { IRocketChatRecord } from './IRocketChatRecord'; import { IUser } from './IUser'; +import { IServerRoomItem } from './IRoom'; export enum TEAM_TYPE { PUBLIC = 0, @@ -46,3 +47,16 @@ export interface ITeamStats { totalTeams: number; teamStats: Array; } + +export interface IServerTeamUpdateRoom + extends Omit< + IServerRoomItem, + 'topic' | 'joinCodeRequired' | 'description' | 'jitsiTimeout' | 'usersCount' | 'e2eKeyId' | 'avatarETag' + > { + broadcast: boolean; + msgs: number; + default: boolean; + sysMes: boolean; + teamId: string; + teamDefault: boolean; +} diff --git a/app/definitions/IUser.ts b/app/definitions/IUser.ts index e1746554d..7a82aa64e 100644 --- a/app/definitions/IUser.ts +++ b/app/definitions/IUser.ts @@ -103,6 +103,22 @@ export interface IUserSettings { [key: string]: any; }; } +type TNotifications = 'default' | 'all' | 'mentions' | 'nothing'; + +export interface INotificationPreferences { + id: string; + enableMessageParserEarlyAdoption: boolean; + desktopNotifications: TNotifications; + pushNotifications: TNotifications; + emailNotificationMode?: 'mentions' | 'nothing'; +} + +export interface IUserPreferences { + user: { _id: string }; + settings: { + preferences: INotificationPreferences; + }; +} export interface IUser extends IRocketChatRecord, Omit { _id: string; diff --git a/app/definitions/IVisitor.ts b/app/definitions/IVisitor.ts deleted file mode 100644 index 637513567..000000000 --- a/app/definitions/IVisitor.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface IVisitorEmail { - address: string; -} - -export interface IVisitorPhone { - phoneNumber: string; -} - -export interface IVisitor { - _id?: string; - token: string; - username: string; - updatedAt?: Date; - name: string; - department?: string; - phone?: IVisitorPhone[]; - visitorEmails?: IVisitorEmail[]; - customFields?: { - [key: string]: any; - }; - livechatData: { - [key: string]: any; - }; -} diff --git a/app/definitions/rest/v1/channels.ts b/app/definitions/rest/v1/channels.ts index 640ade486..6692f0a9c 100644 --- a/app/definitions/rest/v1/channels.ts +++ b/app/definitions/rest/v1/channels.ts @@ -29,6 +29,12 @@ export type ChannelsEndpoints = { messages: IMessageFromServer[]; }; }; + 'channels.archive': { + POST: (params: { roomId: string }) => void; + }; + 'channels.unarchive': { + POST: (params: { roomId: string }) => void; + }; 'channels.create': { POST: (params: { name: string; diff --git a/app/definitions/rest/v1/chat.ts b/app/definitions/rest/v1/chat.ts index 4a224c475..6b4253966 100644 --- a/app/definitions/rest/v1/chat.ts +++ b/app/definitions/rest/v1/chat.ts @@ -32,4 +32,11 @@ export type ChatEndpoints = { total: number; }>; }; + 'chat.delete': { + POST: (params: { msgId: string; roomId: string }) => { + _id: string; + ts: string; + message: Pick; + }; + }; }; diff --git a/app/definitions/rest/v1/e2e.ts b/app/definitions/rest/v1/e2e.ts index b62f312f5..44705c851 100644 --- a/app/definitions/rest/v1/e2e.ts +++ b/app/definitions/rest/v1/e2e.ts @@ -9,4 +9,10 @@ export type E2eEndpoints = { users: Pick[]; }; }; + 'e2e.updateGroupKey': { + POST: (params: { uid: string; rid: string; key: string }) => {}; + }; + 'e2e.setRoomKeyID': { + POST: (params: { rid: string; keyID: string }) => {}; + }; }; diff --git a/app/definitions/rest/v1/groups.ts b/app/definitions/rest/v1/groups.ts index 8868ac7de..966a6759a 100644 --- a/app/definitions/rest/v1/groups.ts +++ b/app/definitions/rest/v1/groups.ts @@ -23,6 +23,12 @@ export type GroupsEndpoints = { messages: IMessageFromServer[]; }; }; + 'groups.archive': { + POST: (params: { roomId: string }) => void; + }; + 'groups.unarchive': { + POST: (params: { roomId: string }) => void; + }; 'groups.create': { POST: (params: { name: string; diff --git a/app/definitions/rest/v1/index.ts b/app/definitions/rest/v1/index.ts index e292cb99b..61f19572e 100644 --- a/app/definitions/rest/v1/index.ts +++ b/app/definitions/rest/v1/index.ts @@ -11,7 +11,6 @@ import { PermissionsEndpoints } from './permissions'; import { RolesEndpoints } from './roles'; import { RoomsEndpoints } from './rooms'; import { OauthCustomConfiguration } from './settings'; -import { UserEndpoints } from './user'; import { UsersEndpoints } from './users'; import { TeamsEndpoints } from './teams'; import { E2eEndpoints } from './e2e'; @@ -30,7 +29,6 @@ export type Endpoints = ChannelsEndpoints & RolesEndpoints & RoomsEndpoints & OauthCustomConfiguration & - UserEndpoints & UsersEndpoints & TeamsEndpoints & E2eEndpoints & diff --git a/app/definitions/rest/v1/omnichannel.ts b/app/definitions/rest/v1/omnichannel.ts index 3a7d6bcec..7b5dcb64d 100644 --- a/app/definitions/rest/v1/omnichannel.ts +++ b/app/definitions/rest/v1/omnichannel.ts @@ -20,11 +20,7 @@ export type OmnichannelEndpoints = { }; 'livechat/visitors.info': { GET: (params: { visitorId: string }) => { - visitor: { - visitorEmails: Array<{ - address: string; - }>; - }; + visitor: ILivechatVisitor; }; }; 'livechat/room.onHold': { @@ -77,7 +73,7 @@ export type OmnichannelEndpoints = { POST: (params: { upsert: string[]; remove: string[] }) => void; }; 'livechat/department/:departmentId/?includeAgents=false': { - GET: () => PaginatedResult<{ department: ILivechatDepartment[] }>; + GET: () => PaginatedResult<{ department: ILivechatDepartment }>; }; 'livechat/departments.available-by-unit/:id': { GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ diff --git a/app/definitions/rest/v1/subscriptions.ts b/app/definitions/rest/v1/subscriptions.ts index c05e25cf2..d02c81099 100644 --- a/app/definitions/rest/v1/subscriptions.ts +++ b/app/definitions/rest/v1/subscriptions.ts @@ -1,5 +1,8 @@ export type SubscriptionsEndpoints = { 'subscriptions.unread': { - POST: (params: { firstUnreadMessage: { _id: string } }) => {}; + POST: (params: { firstUnreadMessage: { _id: string } } | { roomId: string }) => {}; + }; + 'subscriptions.read': { + POST: (params: { rid: string }) => {}; }; }; diff --git a/app/definitions/rest/v1/teams.ts b/app/definitions/rest/v1/teams.ts index cc6bfd620..e32c5252a 100644 --- a/app/definitions/rest/v1/teams.ts +++ b/app/definitions/rest/v1/teams.ts @@ -1,9 +1,29 @@ -import { IRoom } from '../../IRoom'; -import { ITeam, TEAM_TYPE } from '../../ITeam'; +import { IRoom, IServerRoomItem } from '../../IRoom'; +import { IServerTeamUpdateRoom, ITeam, TEAM_TYPE } from '../../ITeam'; export type TeamsEndpoints = { 'teams.removeRoom': { - POST: (params: { roomId: string; teamId: string }) => { room: IRoom }; + POST: (params: { roomId: string; teamId: string }) => { room: IServerRoomItem }; + }; + 'teams.listRoomsOfUser': { + GET: (params: { teamId: string; userId: string }) => { + rooms: IServerRoomItem[]; + total: number; + count: number; + offset: number; + }; + }; + 'teams.updateRoom': { + POST: (params: { roomId: string; isDefault: boolean }) => { room: IServerTeamUpdateRoom }; + }; + 'teams.convertToChannel': { + POST: (params: { teamId: string; roomsToRemove?: string[] }) => {}; + }; + 'teams.removeMember': { + POST: (params: { teamId: string; userId: string; rooms?: string[] }) => {}; + }; + 'teams.addRooms': { + POST: (params: { teamId: string; rooms: string[] }) => { rooms: IRoom[] }; }; 'teams.create': { POST: (params: { diff --git a/app/definitions/rest/v1/user.ts b/app/definitions/rest/v1/user.ts deleted file mode 100644 index b8c37cc17..000000000 --- a/app/definitions/rest/v1/user.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { IUser, IUserRegistered } from '../../IUser'; - -export type UserEndpoints = { - 'users.info': { - GET: (params: { userId: IUser['_id'] }) => { - user: IUser; - success: boolean; - }; - POST: (params: { userId: IUser['_id'] }) => { - user: IUser; - success: boolean; - }; - }; - 'users.register': { - POST: (params: { name: string; email: string; username: string; pass: string }) => { user: IUserRegistered }; - }; -}; diff --git a/app/definitions/rest/v1/users.ts b/app/definitions/rest/v1/users.ts index 337a2182f..251ff3ded 100644 --- a/app/definitions/rest/v1/users.ts +++ b/app/definitions/rest/v1/users.ts @@ -1,5 +1,6 @@ import type { ITeam } from '../../ITeam'; import type { IUser } from '../../IUser'; +import { INotificationPreferences, IUserPreferences, IUserRegistered } from '../../IUser'; export type UsersEndpoints = { 'users.2fa.sendEmailCode': { @@ -11,4 +12,26 @@ export type UsersEndpoints = { 'users.listTeams': { GET: (params: { userId: IUser['_id'] }) => { teams: Array }; }; + 'users.forgotPassword': { + POST: (params: { email: string }) => {}; + }; + 'users.info': { + GET: (params: { userId: IUser['_id'] }) => { + user: IUser; + success: boolean; + }; + POST: (params: { userId: IUser['_id'] }) => { + user: IUser; + success: boolean; + }; + }; + 'users.setPreferences': { + POST: (params: { userId: IUser['_id']; data: Partial }) => { + user: IUserPreferences; + success: boolean; + }; + }; + 'users.register': { + POST: (params: { name: string; email: string; username: string; pass: string }) => { user: IUserRegistered }; + }; }; diff --git a/app/lib/methods/helpers/buildMessage.ts b/app/lib/methods/helpers/buildMessage.ts index b17574c97..a0fc06013 100644 --- a/app/lib/methods/helpers/buildMessage.ts +++ b/app/lib/methods/helpers/buildMessage.ts @@ -1,8 +1,8 @@ -import { IMessage, IThreadResult } from '../../../definitions'; +import { ILastMessage, IMessage, IThreadResult } from '../../../definitions'; import messagesStatus from '../../../constants/messagesStatus'; import normalizeMessage from './normalizeMessage'; -export default (message: Partial | IThreadResult): Partial | IThreadResult => { +export default (message: Partial | IThreadResult | ILastMessage): IMessage | IThreadResult => { message.status = messagesStatus.SENT; return normalizeMessage(message); }; diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.ts b/app/lib/methods/helpers/mergeSubscriptionsRooms.ts index 4cca2707d..da2485ee8 100644 --- a/app/lib/methods/helpers/mergeSubscriptionsRooms.ts +++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.ts @@ -5,12 +5,19 @@ import { store as reduxStore } from '../../auxStore'; import { compareServerVersion } from '../../utils'; import findSubscriptionsRooms from './findSubscriptionsRooms'; import normalizeMessage from './normalizeMessage'; -import { ISubscription, IServerRoom, IServerSubscription, IServerSubscriptionItem, IServerRoomItem } from '../../../definitions'; +import { + ISubscription, + IServerRoom, + IServerSubscription, + IServerSubscriptionItem, + IServerRoomItem, + IRoom +} from '../../../definitions'; // TODO: delete and update export const merge = ( subscription: ISubscription | IServerSubscriptionItem, - room?: ISubscription | IServerRoomItem + room?: ISubscription | IServerRoomItem | IRoom ): ISubscription => { const serverVersion = reduxStore.getState().server.version as string; subscription = EJSON.fromJSONValue(subscription) as ISubscription; diff --git a/app/lib/methods/loadMessagesForRoom.ts b/app/lib/methods/loadMessagesForRoom.ts index b72371bfb..f2bce7102 100644 --- a/app/lib/methods/loadMessagesForRoom.ts +++ b/app/lib/methods/loadMessagesForRoom.ts @@ -1,6 +1,7 @@ import moment from 'moment'; -import { IMessage, MessageType, TMessageModel } from '../../definitions'; +import { MessageTypeLoad } from '../../constants/messageTypeLoad'; +import { IMessage, TMessageModel } from '../../definitions'; import log from '../../utils/log'; import { getMessageById } from '../database/services/Message'; import roomTypeToApiType, { RoomTypes } from '../rocketchat/methods/roomTypeToApiType'; @@ -46,7 +47,7 @@ export default function loadMessagesForRoom(args: { _id: generateLoadMoreId(lastMessage._id as string), rid: lastMessage.rid, ts: moment(lastMessage.ts).subtract(1, 'millisecond').toString(), - t: 'load_more' as MessageType, + t: MessageTypeLoad.MORE, msg: lastMessage.msg }; data.push(loadMoreMessage); diff --git a/app/lib/methods/loadMissedMessages.ts b/app/lib/methods/loadMissedMessages.ts index 2674e8a19..5a114269c 100644 --- a/app/lib/methods/loadMissedMessages.ts +++ b/app/lib/methods/loadMissedMessages.ts @@ -16,7 +16,7 @@ const getLastUpdate = async (rid: string) => { return null; }; -async function load({ rid: roomId, lastOpen }: { rid: string; lastOpen: Date }) { +async function load({ rid: roomId, lastOpen }: { rid: string; lastOpen?: Date }) { let lastUpdate; if (lastOpen) { lastUpdate = new Date(lastOpen).toISOString(); @@ -29,7 +29,7 @@ async function load({ rid: roomId, lastOpen }: { rid: string; lastOpen: Date }) return result; } -export default function loadMissedMessages(args: { rid: string; lastOpen: Date }): Promise { +export default function loadMissedMessages(args: { rid: string; lastOpen?: Date }): Promise { return new Promise(async (resolve, reject) => { try { const data = await load({ rid: args.rid, lastOpen: args.lastOpen }); diff --git a/app/lib/methods/loadNextMessages.ts b/app/lib/methods/loadNextMessages.ts index 45a007d63..063170652 100644 --- a/app/lib/methods/loadNextMessages.ts +++ b/app/lib/methods/loadNextMessages.ts @@ -4,7 +4,7 @@ import orderBy from 'lodash/orderBy'; import log from '../../utils/log'; import { getMessageById } from '../database/services/Message'; -import { MESSAGE_TYPE_LOAD_NEXT_CHUNK } from '../../constants/messageTypeLoad'; +import { MessageTypeLoad } from '../../constants/messageTypeLoad'; import { generateLoadMoreId } from '../utils'; import updateMessages from './updateMessages'; import { TMessageModel } from '../../definitions'; @@ -34,7 +34,7 @@ export default function loadNextMessages(args: ILoadNextMessages): Promise rid: lastMessage.rid, tmid: args.tmid, ts: moment(lastMessage.ts).add(1, 'millisecond'), - t: MESSAGE_TYPE_LOAD_NEXT_CHUNK + t: MessageTypeLoad.NEXT_CHUNK }; messages.push(loadMoreItem); } diff --git a/app/lib/methods/loadSurroundingMessages.js b/app/lib/methods/loadSurroundingMessages.ts similarity index 67% rename from app/lib/methods/loadSurroundingMessages.js rename to app/lib/methods/loadSurroundingMessages.ts index bc25ae695..727bd200e 100644 --- a/app/lib/methods/loadSurroundingMessages.js +++ b/app/lib/methods/loadSurroundingMessages.ts @@ -4,21 +4,23 @@ import orderBy from 'lodash/orderBy'; import log from '../../utils/log'; import { getMessageById } from '../database/services/Message'; -import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../constants/messageTypeLoad'; +import { MessageTypeLoad } from '../../constants/messageTypeLoad'; +import sdk from '../rocketchat/services/sdk'; +import { IMessage } from '../../definitions'; import { generateLoadMoreId } from '../utils'; import updateMessages from './updateMessages'; const COUNT = 50; -export default function loadSurroundingMessages({ messageId, rid }) { +export default function loadSurroundingMessages({ messageId, rid }: { messageId: string; rid: string }) { return new Promise(async (resolve, reject) => { try { - const data = await this.methodCallWrapper('loadSurroundingMessages', { _id: messageId, rid }, COUNT); - let messages = EJSON.fromJSONValue(data?.messages); + const data = await sdk.methodCallWrapper('loadSurroundingMessages', { _id: messageId, rid }, COUNT); + let messages: IMessage[] = EJSON.fromJSONValue(data?.messages); messages = orderBy(messages, 'ts'); const message = messages.find(m => m._id === messageId); - const { tmid } = message; + const tmid = message?.tmid; if (messages?.length) { if (data?.moreBefore) { @@ -29,10 +31,10 @@ export default function loadSurroundingMessages({ messageId, rid }) { _id: generateLoadMoreId(firstMessage._id), rid: firstMessage.rid, tmid, - ts: moment(firstMessage.ts).subtract(1, 'millisecond'), - t: MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, + ts: moment(firstMessage.ts).subtract(1, 'millisecond').toDate(), + t: MessageTypeLoad.PREVIOUS_CHUNK, msg: firstMessage.msg - }; + } as IMessage; messages.unshift(loadMoreItem); } } @@ -45,18 +47,18 @@ export default function loadSurroundingMessages({ messageId, rid }) { _id: generateLoadMoreId(lastMessage._id), rid: lastMessage.rid, tmid, - ts: moment(lastMessage.ts).add(1, 'millisecond'), - t: MESSAGE_TYPE_LOAD_NEXT_CHUNK, + ts: moment(lastMessage.ts).add(1, 'millisecond').toDate(), + t: MessageTypeLoad.NEXT_CHUNK, msg: lastMessage.msg - }; + } as IMessage; messages.push(loadMoreItem); } } + await updateMessages({ rid, update: messages }); return resolve(messages); - } else { - return resolve([]); } + return resolve([]); } catch (e) { log(e); reject(e); diff --git a/app/lib/methods/logout.ts b/app/lib/methods/logout.ts index fefd1769b..0e53ed123 100644 --- a/app/lib/methods/logout.ts +++ b/app/lib/methods/logout.ts @@ -13,7 +13,7 @@ import { E2E_PRIVATE_KEY, E2E_PUBLIC_KEY, E2E_RANDOM_PASSWORD_KEY } from '../enc import UserPreferences from '../userPreferences'; import { ICertificate, IRocketChat } from '../../definitions'; -async function removeServerKeys({ server, userId }: { server: string; userId: string | null }) { +async function removeServerKeys({ server, userId }: { server: string; userId?: string | null }) { await UserPreferences.removeItem(`${RocketChat.TOKEN_KEY}-${server}`); if (userId) { await UserPreferences.removeItem(`${RocketChat.TOKEN_KEY}-${userId}`); diff --git a/app/lib/methods/subscriptions/room.js b/app/lib/methods/subscriptions/room.ts similarity index 78% rename from app/lib/methods/subscriptions/room.js rename to app/lib/methods/subscriptions/room.ts index 3aa0a206d..f72137c22 100644 --- a/app/lib/methods/subscriptions/room.js +++ b/app/lib/methods/subscriptions/room.ts @@ -15,11 +15,30 @@ import debounce from '../../../utils/debounce'; import RocketChat from '../../rocketchat'; import { subscribeRoom, unsubscribeRoom } from '../../../actions/room'; import { Encryption } from '../../encryption'; +import { IMessage, TMessageModel, TSubscriptionModel, TThreadMessageModel, TThreadModel } from '../../../definitions'; +import { IDDPMessage } from '../../../definitions/IDDPMessage'; const WINDOW_TIME = 1000; export default class RoomSubscription { - constructor(rid) { + private rid: string; + private isAlive: boolean; + private timer: null | number; + private queue: { [key: string]: IMessage }; + private messagesBatch: {}; + private _messagesBatch: { [key: string]: TMessageModel }; + private threadsBatch: {}; + private _threadsBatch: { [key: string]: TThreadModel }; + private threadMessagesBatch: {}; + private _threadMessagesBatch: { [key: string]: TThreadMessageModel }; + private promises?: Promise; + private connectedListener?: Promise; + private disconnectedListener?: Promise; + private notifyRoomListener?: Promise; + private messageReceivedListener?: Promise; + private lastOpen?: Date; + + constructor(rid: string) { this.rid = rid; this.isAlive = true; this.timer = null; @@ -27,6 +46,10 @@ export default class RoomSubscription { this.messagesBatch = {}; this.threadsBatch = {}; this.threadMessagesBatch = {}; + + this._messagesBatch = {}; + this._threadsBatch = {}; + this._threadMessagesBatch = {}; } subscribe = async () => { @@ -41,7 +64,7 @@ export default class RoomSubscription { this.notifyRoomListener = RocketChat.onStreamData('stream-notify-room', this.handleNotifyRoomReceived); this.messageReceivedListener = RocketChat.onStreamData('stream-room-messages', this.handleMessageReceived); if (!this.isAlive) { - this.unsubscribe(); + await this.unsubscribe(); } reduxStore.dispatch(subscribeRoom(this.rid)); @@ -69,7 +92,7 @@ export default class RoomSubscription { } }; - removeListener = async promise => { + removeListener = async (promise?: Promise): Promise => { if (promise) { try { const listener = await promise; @@ -85,7 +108,7 @@ export default class RoomSubscription { RocketChat.loadMissedMessages({ rid: this.rid }).catch(e => console.log(e)); }; - handleNotifyRoomReceived = protectedFunction(ddpMessage => { + handleNotifyRoomReceived = protectedFunction((ddpMessage: IDDPMessage) => { const [_rid, ev] = ddpMessage.fields.eventName.split('/'); if (this.rid !== _rid) { return; @@ -115,9 +138,9 @@ export default class RoomSubscription { const msgCollection = db.get('messages'); const threadsCollection = db.get('threads'); const threadMessagesCollection = db.get('thread_messages'); - let deleteMessage; - let deleteThread; - let deleteThreadMessage; + let deleteMessage: TMessageModel; + let deleteThread: TThreadModel; + let deleteThreadMessage: TThreadMessageModel; // Delete message try { @@ -142,7 +165,7 @@ export default class RoomSubscription { } catch (e) { // Do nothing } - await db.action(async () => { + await db.write(async () => { await db.batch(deleteMessage, deleteThread, deleteThreadMessage); }); } catch (e) { @@ -153,11 +176,11 @@ export default class RoomSubscription { } }); - read = debounce(lastOpen => { + read = debounce((lastOpen: Date) => { RocketChat.readMessages(this.rid, lastOpen); }, 300); - updateMessage = message => + updateMessage = (message: IMessage): Promise => new Promise(async resolve => { if (this.rid !== message.rid) { return resolve(); @@ -177,15 +200,15 @@ export default class RoomSubscription { const messageRecord = await getMessageById(message._id); if (messageRecord) { operation = messageRecord.prepareUpdate( - protectedFunction(m => { + protectedFunction((m: TMessageModel) => { Object.assign(m, message); }) ); } else { operation = msgCollection.prepareCreate( - protectedFunction(m => { + protectedFunction((m: TMessageModel) => { m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema); - m.subscription.id = this.rid; + if (m.subscription) m.subscription.id = this.rid; Object.assign(m, message); }) ); @@ -202,15 +225,15 @@ export default class RoomSubscription { const threadRecord = await getThreadById(message._id); if (threadRecord) { operation = threadRecord.prepareUpdate( - protectedFunction(t => { + protectedFunction((t: TThreadModel) => { Object.assign(t, message); }) ); } else { operation = threadsCollection.prepareCreate( - protectedFunction(t => { + protectedFunction((t: TThreadModel) => { t._raw = sanitizedRaw({ id: message._id }, threadsCollection.schema); - t.subscription.id = this.rid; + if (t.subscription) t.subscription.id = this.rid; Object.assign(t, message); }) ); @@ -228,20 +251,26 @@ export default class RoomSubscription { const threadMessageRecord = await getThreadMessageById(message._id); if (threadMessageRecord) { operation = threadMessageRecord.prepareUpdate( - protectedFunction(tm => { + protectedFunction((tm: TThreadMessageModel) => { Object.assign(tm, message); - tm.rid = message.tmid; - delete tm.tmid; + if (message.tmid) { + tm.rid = message.tmid; + delete tm.tmid; + } }) ); } else { operation = threadMessagesCollection.prepareCreate( - protectedFunction(tm => { + protectedFunction((tm: TThreadMessageModel) => { tm._raw = sanitizedRaw({ id: message._id }, threadMessagesCollection.schema); Object.assign(tm, message); - tm.subscription.id = this.rid; - tm.rid = message.tmid; - delete tm.tmid; + if (tm.subscription) { + tm.subscription.id = this.rid; + } + if (message.tmid) { + tm.rid = message.tmid; + delete tm.tmid; + } }) ); } @@ -254,7 +283,7 @@ export default class RoomSubscription { return resolve(); }); - handleMessageReceived = ddpMessage => { + handleMessageReceived = (ddpMessage: IDDPMessage) => { if (!this.timer) { this.timer = setTimeout(async () => { // copy variables values to local and clean them @@ -280,7 +309,7 @@ export default class RoomSubscription { try { const db = database.active; - await db.action(async () => { + await db.write(async () => { await db.batch( ...Object.values(this._messagesBatch), ...Object.values(this._threadsBatch), @@ -300,7 +329,7 @@ export default class RoomSubscription { }, WINDOW_TIME); } this.lastOpen = new Date(); - const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0])); + const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0])) as IMessage; this.queue[message._id] = message; }; } diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.ts similarity index 77% rename from app/lib/methods/subscriptions/rooms.js rename to app/lib/methods/subscriptions/rooms.ts index 14cf75d5c..ababd496b 100644 --- a/app/lib/methods/subscriptions/rooms.js +++ b/app/lib/methods/subscriptions/rooms.ts @@ -1,11 +1,11 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { InteractionManager } from 'react-native'; import EJSON from 'ejson'; +import Model from '@nozbe/watermelondb/Model'; import database from '../../database'; import { merge } from '../helpers/mergeSubscriptionsRooms'; import protectedFunction from '../helpers/protectedFunction'; -import messagesStatus from '../../../constants/messagesStatus'; import log from '../../../utils/log'; import random from '../../../utils/random'; import { store } from '../../auxStore'; @@ -19,16 +19,29 @@ import { INAPP_NOTIFICATION_EMITTER } from '../../../containers/InAppNotificatio import { Encryption } from '../../encryption'; import { E2E_MESSAGE_TYPE } from '../../encryption/constants'; import updateMessages from '../updateMessages'; +import { + IMessage, + IRoom, + ISubscription, + TMessageModel, + TRoomModel, + TThreadMessageModel, + TThreadModel +} from '../../../definitions'; +import sdk from '../../rocketchat/services/sdk'; +import { IDDPMessage } from '../../../definitions/IDDPMessage'; +import { getSubscriptionByRoomId } from '../../database/services/Subscription'; +import { getMessageById } from '../../database/services/Message'; -const removeListener = listener => listener.stop(); +const removeListener = (listener: { stop: () => void }) => listener.stop(); -let streamListener; -let subServer; -let queue = {}; -let subTimer = null; +let streamListener: Promise | false; +let subServer: string; +let queue: { [key: string]: ISubscription } = {}; +let subTimer: number | null | false = null; const WINDOW_TIME = 500; -const createOrUpdateSubscription = async (subscription, room) => { +const createOrUpdateSubscription = async (subscription: ISubscription, room: IRoom | ISubscription) => { try { const db = database.active; const subCollection = db.get('subscriptions'); @@ -86,12 +99,12 @@ const createOrUpdateSubscription = async (subscription, room) => { e2eKeyId: s.e2eKeyId, E2EKey: s.E2EKey, avatarETag: s.avatarETag - }; + } as ISubscription; } catch (error) { try { - await db.action(async () => { + await db.write(async () => { await roomsCollection.create( - protectedFunction(r => { + protectedFunction((r: TRoomModel) => { r._raw = sanitizedRaw({ id: room._id }, roomsCollection.schema); Object.assign(r, room); }) @@ -121,23 +134,15 @@ const createOrUpdateSubscription = async (subscription, room) => { departmentId: r.departmentId, livechatData: r.livechatData, avatarETag: r.avatarETag - }; + } as IRoom; } catch (error) { // Do nothing } } - let tmp; - if (subscription) { - tmp = merge(subscription, room); - tmp = await Encryption.decryptSubscription(tmp); - } - let sub; - try { - sub = await subCollection.find(tmp.rid); - } catch (error) { - // Do nothing - } + let tmp = merge(subscription, room); + tmp = (await Encryption.decryptSubscription(tmp)) as ISubscription; + const sub = await getSubscriptionByRoomId(tmp.rid); // If we're receiving a E2EKey of a room if (sub && !sub.E2EKey && subscription?.E2EKey) { @@ -151,12 +156,12 @@ const createOrUpdateSubscription = async (subscription, room) => { e2eKeyId: sub.e2eKeyId }); // Decrypt lastMessage using the received E2EKey - tmp = await Encryption.decryptSubscription(tmp); + tmp = (await Encryption.decryptSubscription(tmp)) as ISubscription; // Decrypt all pending messages of this room in parallel Encryption.decryptPendingMessages(tmp.rid); } - const batch = []; + const batch: Model[] = []; if (sub) { try { const update = sub.prepareUpdate(s => { @@ -190,12 +195,7 @@ const createOrUpdateSubscription = async (subscription, room) => { if (tmp.lastMessage && !rooms.includes(tmp.rid)) { const lastMessage = buildMessage(tmp.lastMessage); const messagesCollection = db.get('messages'); - let messageRecord; - try { - messageRecord = await messagesCollection.find(lastMessage._id); - } catch (error) { - // Do nothing - } + const messageRecord = await getMessageById(lastMessage._id); if (messageRecord) { batch.push( @@ -207,14 +207,16 @@ const createOrUpdateSubscription = async (subscription, room) => { batch.push( messagesCollection.prepareCreate(m => { m._raw = sanitizedRaw({ id: lastMessage._id }, messagesCollection.schema); - m.subscription.id = lastMessage.rid; + if (m.subscription) { + m.subscription.id = lastMessage.rid; + } return Object.assign(m, lastMessage); }) ); } } - await db.action(async () => { + await db.write(async () => { await db.batch(...batch); }); } catch (e) { @@ -222,11 +224,11 @@ const createOrUpdateSubscription = async (subscription, room) => { } }; -const getSubQueueId = rid => `SUB-${rid}`; +const getSubQueueId = (rid: string) => `SUB-${rid}`; -const getRoomQueueId = rid => `ROOM-${rid}`; +const getRoomQueueId = (rid: string) => `ROOM-${rid}`; -const debouncedUpdate = subscription => { +const debouncedUpdate = (subscription: ISubscription) => { if (!subTimer) { subTimer = setTimeout(() => { const batch = queue; @@ -257,11 +259,11 @@ const debouncedUpdate = subscription => { }; export default function subscribeRooms() { - const handleStreamMessageReceived = protectedFunction(async ddpMessage => { + const handleStreamMessageReceived = protectedFunction(async (ddpMessage: IDDPMessage) => { const db = database.active; // check if the server from variable is the same as the js sdk client - if (this.sdk && this.sdk.client && this.sdk.client.host !== subServer) { + if (sdk && sdk.current.client && sdk.current.client.host !== subServer) { return; } if (ddpMessage.msg === 'added') { @@ -274,7 +276,7 @@ export default function subscribeRooms() { if (diff?.statusLivechat) { store.dispatch(setUser({ statusLivechat: diff.statusLivechat })); } - if (['settings.preferences.showMessageInMainThread'] in diff) { + if ((['settings.preferences.showMessageInMainThread'] as any) in diff) { store.dispatch(setUser({ showMessageInMainThread: diff['settings.preferences.showMessageInMainThread'] })); } } @@ -283,13 +285,19 @@ export default function subscribeRooms() { try { const subCollection = db.get('subscriptions'); const sub = await subCollection.find(data.rid); - const messages = await sub.messages.fetch(); - const threads = await sub.threads.fetch(); - const threadMessages = await sub.threadMessages.fetch(); - const messagesToDelete = messages.map(m => m.prepareDestroyPermanently()); - const threadsToDelete = threads.map(m => m.prepareDestroyPermanently()); - const threadMessagesToDelete = threadMessages.map(m => m.prepareDestroyPermanently()); - await db.action(async () => { + // TODO - today the Relation type from watermelon just support one to one relations + // @ts-ignore + const messages = (await sub.messages.fetch()) as TMessageModel[]; + // @ts-ignore + const threads = (await sub.threads.fetch()) as TThreadModel[]; + // @ts-ignore + const threadMessages = (await sub.threadMessages.fetch()) as TThreadMessageModel[]; + + const messagesToDelete = messages?.map((m: TMessageModel) => m.prepareDestroyPermanently()); + const threadsToDelete = threads?.map((m: TThreadModel) => m.prepareDestroyPermanently()); + const threadMessagesToDelete = threadMessages?.map((m: TThreadMessageModel) => m.prepareDestroyPermanently()); + + await db.write(async () => { await db.batch(sub.prepareDestroyPermanently(), ...messagesToDelete, ...threadsToDelete, ...threadMessagesToDelete); }); @@ -318,13 +326,14 @@ export default function subscribeRooms() { const [args] = ddpMessage.fields.args; const _id = random(17); const message = { + // @ts-ignore u: { _id, username: 'rocket.cat', name: 'Rocket Cat' }, ...buildMessage(EJSON.fromJSONValue(args)) - }; + } as IMessage; await updateMessages({ rid: args.rid, update: [message] }); } catch (e) { log(e); @@ -383,12 +392,12 @@ export default function subscribeRooms() { } }; - streamListener = this.sdk.onStreamData('stream-notify-user', handleStreamMessageReceived); + streamListener = sdk.onStreamData('stream-notify-user', handleStreamMessageReceived); try { // set the server that started this task - subServer = this.sdk.client.host; - this.sdk.subscribeNotifyUser().catch(e => console.log(e)); + subServer = sdk.current.client.host; + sdk.current.subscribeNotifyUser().catch((e: unknown) => console.log(e)); return { stop: () => stop() diff --git a/app/lib/rocketchat/methods/roomTypeToApiType.ts b/app/lib/rocketchat/methods/roomTypeToApiType.ts index b66b78d2c..4473c5d38 100644 --- a/app/lib/rocketchat/methods/roomTypeToApiType.ts +++ b/app/lib/rocketchat/methods/roomTypeToApiType.ts @@ -4,17 +4,25 @@ enum ETypes { Groups = 'groups' } -export const types = { +export type RoomTypes = 'c' | 'd' | 'p' | 'l'; + +type ApiTypes = T extends 'c' + ? ETypes.Channels + : T extends 'd' + ? ETypes.Im + : T extends 'p' + ? ETypes.Groups + : T extends 'l' + ? ETypes.Channels + : never; + +export const types: { [K in RoomTypes]: ApiTypes } = { c: ETypes.Channels, d: ETypes.Im, p: ETypes.Groups, l: ETypes.Channels }; -// TODO: refactor this -export type RoomTypes = keyof typeof types; -type ApiTypes = typeof types[RoomTypes]; - -const roomTypeToApiType = (t: RoomTypes): ApiTypes => types[t]; +const roomTypeToApiType = (t: T) => types[t]; export default roomTypeToApiType; diff --git a/app/lib/rocketchat/services/restApi.ts b/app/lib/rocketchat/services/restApi.ts index b5947efae..40db3b020 100644 --- a/app/lib/rocketchat/services/restApi.ts +++ b/app/lib/rocketchat/services/restApi.ts @@ -1,7 +1,7 @@ -import { SubscriptionType } from '../../../definitions'; +import sdk from './sdk'; import { TEAM_TYPE } from '../../../definitions/ITeam'; import roomTypeToApiType, { RoomTypes } from '../methods/roomTypeToApiType'; -import sdk from './sdk'; +import { SubscriptionType, INotificationPreferences } from '../../../definitions'; export const createChannel = ({ name, @@ -37,7 +37,7 @@ export const e2eSetUserPublicAndPrivateKeys = (public_key: string, private_key: // RC 2.2.0 sdk.post('e2e.setUserPublicAndPrivateKeys', { public_key, private_key }); -export const e2eRequestSubscriptionKeys = (): any => +export const e2eRequestSubscriptionKeys = (): Promise => // RC 0.72.0 sdk.methodCallWrapper('e2e.requestSubscriptionKeys'); @@ -45,16 +45,12 @@ export const e2eGetUsersOfRoomWithoutKey = (rid: string) => // RC 0.70.0 sdk.get('e2e.getUsersOfRoomWithoutKey', { rid }); -export const e2eSetRoomKeyID = (rid: string, keyID: string): any => +export const e2eSetRoomKeyID = (rid: string, keyID: string) => // RC 0.70.0 - // TODO: missing definitions from server - // @ts-ignore sdk.post('e2e.setRoomKeyID', { rid, keyID }); export const e2eUpdateGroupKey = (uid: string, rid: string, key: string): any => // RC 0.70.0 - // TODO: missing definitions from server - // @ts-ignore sdk.post('e2e.updateGroupKey', { uid, rid, key }); export const e2eRequestRoomKey = (rid: string, e2eKeyId: string) => @@ -71,10 +67,8 @@ export const register = (credentials: { name: string; email: string; pass: strin // RC 0.50.0 sdk.post('users.register', credentials); -export const forgotPassword = (email: string): any => +export const forgotPassword = (email: string) => // RC 0.64.0 - // TODO: missing definitions from server - // @ts-ignore sdk.post('users.forgotPassword', { email }); export const sendConfirmationEmail = (email: string): Promise<{ message: string; success: boolean }> => @@ -119,7 +113,7 @@ export const getDiscussions = ({ count, text }: { - roomId: string | undefined; + roomId: string; text?: string | undefined; offset: number; count: number; @@ -164,10 +158,8 @@ export const createTeam = ({ // RC 3.13.0 return sdk.post('teams.create', params); }; -export const addRoomsToTeam = ({ teamId, rooms }: { teamId: string; rooms: string[] }): any => +export const addRoomsToTeam = ({ teamId, rooms }: { teamId: string; rooms: string[] }) => // RC 3.13.0 - // TODO: missing definitions from server - // @ts-ignore sdk.post('teams.addRooms', { teamId, rooms }); export const removeTeamRoom = ({ roomId, teamId }: { roomId: string; teamId: string }) => @@ -184,10 +176,8 @@ export const leaveTeam = ({ teamId, rooms }: { teamId: string; rooms: string[] } ...(rooms?.length && { rooms }) }); -export const removeTeamMember = ({ teamId, userId, rooms }: { teamId: string; userId: string; rooms: string[] }): any => +export const removeTeamMember = ({ teamId, userId, rooms }: { teamId: string; userId: string; rooms: string[] }) => // RC 3.13.0 - // TODO: missing definitions from server - // @ts-ignore sdk.post('teams.removeMember', { teamId, userId, @@ -195,10 +185,8 @@ export const removeTeamMember = ({ teamId, userId, rooms }: { teamId: string; us ...(rooms?.length && { rooms }) }); -export const updateTeamRoom = ({ roomId, isDefault }: { roomId: string; isDefault: boolean }): any => +export const updateTeamRoom = ({ roomId, isDefault }: { roomId: string; isDefault: boolean }) => // RC 3.13.0 - // TODO: missing definitions from server - // @ts-ignore sdk.post('teams.updateRoom', { roomId, isDefault }); export const deleteTeam = ({ teamId, roomsToRemove }: { teamId: string; roomsToRemove: string[] }): any => @@ -207,10 +195,8 @@ export const deleteTeam = ({ teamId, roomsToRemove }: { teamId: string; roomsToR // @ts-ignore sdk.post('teams.delete', { teamId, roomsToRemove }); -export const teamListRoomsOfUser = ({ teamId, userId }: { teamId: string; userId: string }): any => +export const teamListRoomsOfUser = ({ teamId, userId }: { teamId: string; userId: string }) => // RC 3.13.0 - // TODO: missing definitions from server - // @ts-ignore sdk.get('teams.listRoomsOfUser', { teamId, userId }); export const convertChannelToTeam = ({ rid, name, type }: { rid: string; name: string; type: 'c' | 'p' }) => { @@ -228,13 +214,11 @@ export const convertChannelToTeam = ({ rid, name, type }: { rid: string; name: s return sdk.post(type === 'c' ? 'channels.convertToTeam' : 'groups.convertToTeam', params); }; -export const convertTeamToChannel = ({ teamId, selected }: { teamId: string; selected: string[] }): any => { +export const convertTeamToChannel = ({ teamId, selected }: { teamId: string; selected: string[] }) => { const params = { teamId, ...(selected.length && { roomsToRemove: selected }) }; - // TODO: missing definitions from server - // @ts-ignore return sdk.post('teams.convertToChannel', params); }; @@ -249,10 +233,8 @@ export const joinRoom = (roomId: string, joinCode: string | null, type: 'c' | 'p return sdk.post('channels.join', { roomId, joinCode }); }; -export const deleteMessage = (messageId: string, rid: string): any => +export const deleteMessage = (messageId: string, rid: string) => // RC 0.48.0 - // TODO: missing definitions from server - // @ts-ignore sdk.post('chat.delete', { msgId: messageId, roomId: rid }); export const markAsUnread = ({ messageId }: { messageId: string }) => @@ -291,10 +273,8 @@ export const reportMessage = (messageId: string): any => // @ts-ignore sdk.post('chat.reportMessage', { messageId, description: 'Message reported by user' }); -export const setUserPreferences = (userId: string, data: any): any => +export const setUserPreferences = (userId: string, data: Partial) => // RC 0.62.0 - // TODO: missing definitions from server - // @ts-ignore sdk.post('users.setPreferences', { userId, data }); export const setUserStatus = (status?: string, message?: string): any => @@ -309,14 +289,10 @@ export const setReaction = (emoji: string, messageId: string): any => // @ts-ignore sdk.post('chat.react', { emoji, messageId }); -export const toggleRead = (read: boolean, roomId: string): any => { +export const toggleRead = (read: boolean, roomId: string) => { if (read) { - // TODO: missing definitions from server - // @ts-ignore return sdk.post('subscriptions.unread', { roomId }); } - // TODO: missing definitions from server - // @ts-ignore return sdk.post('subscriptions.read', { rid: roomId }); }; @@ -461,7 +437,7 @@ export const getListCannedResponse = ({ scope = '', departmentId = '', offset = return sdk.get('canned-responses', params); }; -export const toggleBlockUser = (rid: string, blocked: string, block: boolean) => { +export const toggleBlockUser = (rid: string, blocked: string, block: boolean): Promise => { if (block) { // RC 0.49.0 return sdk.methodCallWrapper('blockUser', { rid, blocked }); @@ -572,17 +548,14 @@ export const ignoreUser = ({ rid, userId, ignore }: { rid: string; userId: strin // @ts-ignore sdk.get('chat.ignoreUser', { rid, userId, ignore }); -export const toggleArchiveRoom = (roomId: string, t: SubscriptionType, archive: boolean): any => { +export const toggleArchiveRoom = (roomId: string, t: SubscriptionType, archive: boolean) => { + const type = t as SubscriptionType.CHANNEL | SubscriptionType.GROUP; if (archive) { // RC 0.48.0 - // TODO: missing definitions from server - // @ts-ignore - return sdk.post(`${roomTypeToApiType(t)}.archive`, { roomId }); + return sdk.post(`${roomTypeToApiType(type)}.archive`, { roomId }); } // RC 0.48.0 - // TODO: missing definitions from server - // @ts-ignore - return sdk.post(`${roomTypeToApiType(t)}.unarchive`, { roomId }); + return sdk.post(`${roomTypeToApiType(type)}.unarchive`, { roomId }); }; export const hideRoom = (roomId: string, t: RoomTypes): any => @@ -591,7 +564,22 @@ export const hideRoom = (roomId: string, t: RoomTypes): any => // @ts-ignore sdk.post(`${roomTypeToApiType(t)}.close`, { roomId }); -export const saveRoomSettings = (rid: string, params: any) => +export const saveRoomSettings = ( + rid: string, + params: { + roomName?: string; + roomAvatar?: string; + roomDescription?: string; + roomTopic?: string; + roomAnnouncement?: string; + roomType?: SubscriptionType; + readOnly?: boolean; + reactWhenReadOnly?: boolean; + systemMessages?: string[]; + joinCode?: string; + encrypted?: boolean; + } +): Promise<{ result: boolean; rid: string }> => // RC 0.55.0 sdk.methodCallWrapper('saveRoomSettings', rid, params); diff --git a/app/stacks/MasterDetailStack/types.ts b/app/stacks/MasterDetailStack/types.ts index 0db458cd9..00fa276a2 100644 --- a/app/stacks/MasterDetailStack/types.ts +++ b/app/stacks/MasterDetailStack/types.ts @@ -39,6 +39,7 @@ export type ModalStackParamList = { member: any; rid: string; t: SubscriptionType; + showCloseModal?: boolean; }; SelectListView: { data: any; diff --git a/app/stacks/types.ts b/app/stacks/types.ts index a1d8dcda6..278f1d55c 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -53,10 +53,11 @@ export type ChatsStackParamList = { isRadio?: boolean; }; RoomInfoView: { - room: ISubscription; + room?: ISubscription; member: any; rid: string; t: SubscriptionType; + showCloseModal?: boolean; }; RoomInfoEditView: { rid: string; diff --git a/app/views/LivechatEditView.tsx b/app/views/LivechatEditView.tsx index 426ae7fb7..842dfc768 100644 --- a/app/views/LivechatEditView.tsx +++ b/app/views/LivechatEditView.tsx @@ -18,7 +18,7 @@ import { getUserSelector } from '../selectors/login'; import Button from '../containers/Button'; import SafeAreaView from '../containers/SafeAreaView'; import { MultiSelect } from '../containers/UIKit/MultiSelect'; -import { IVisitor } from '../definitions/IVisitor'; +import { ILivechatVisitor } from '../definitions/ILivechatVisitor'; import { ITagsOmnichannel } from '../definitions/ITagsOmnichannel'; import { IApplicationState, ISubscription } from '../definitions'; import { ChatsStackParamList } from '../stacks/types'; @@ -55,15 +55,18 @@ interface IField { } interface IInputs { - [key: string]: string | string[] | undefined; + livechatData: { + [key: string]: any; + }; name: string; email: string; phone?: string; topic: string; tag: string[]; + [key: string]: any; } -type TParams = IVisitor & IInputs; +type TParams = ILivechatVisitor & IInputs; interface ILivechat extends ISubscription { // Param dynamic depends on server diff --git a/app/views/RoomActionsView/index.tsx b/app/views/RoomActionsView/index.tsx index 7be3ae706..d3c894014 100644 --- a/app/views/RoomActionsView/index.tsx +++ b/app/views/RoomActionsView/index.tsx @@ -513,20 +513,22 @@ class RoomActionsView extends React.Component ({ - rid: r._id, - name: r.name, - teamId: r.teamId - })); - navigation.navigate('SelectListView', { - title: 'Converting_Team_To_Channel', - data: teamChannels as any, - infoText: 'Select_Team_Channels_To_Delete', - nextAction: (data: string[]) => this.convertTeamToChannelConfirmation(data) - }); - } else { - this.convertTeamToChannelConfirmation(); + if (result.success) { + if (result.rooms?.length) { + const teamChannels = result.rooms.map((r: any) => ({ + rid: r._id, + name: r.name, + teamId: r.teamId + })); + navigation.navigate('SelectListView', { + title: 'Converting_Team_To_Channel', + data: teamChannels as any, + infoText: 'Select_Team_Channels_To_Delete', + nextAction: (data: string[]) => this.convertTeamToChannelConfirmation(data) + }); + } else { + this.convertTeamToChannelConfirmation(); + } } } catch (e) { this.convertTeamToChannelConfirmation(); @@ -572,26 +574,28 @@ class RoomActionsView extends React.Component ({ - rid: r._id, - name: r.name, - teamId: r.teamId, - alert: r.isLastOwner - })); - navigation.navigate('SelectListView', { - title: 'Leave_Team', - data: teamChannels, - infoText: 'Select_Team_Channels', - nextAction: data => dispatch(leaveRoom(ERoomType.t, room, data)), - showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_leave')) - }); - } else { - showConfirmationAlert({ - message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }), - confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), - onPress: () => dispatch(leaveRoom(ERoomType.t, room)) - }); + if (result.success) { + if (result.rooms?.length) { + const teamChannels = result.rooms.map((r: any) => ({ + rid: r._id, + name: r.name, + teamId: r.teamId, + alert: r.isLastOwner + })); + navigation.navigate('SelectListView', { + title: 'Leave_Team', + data: teamChannels as any, + infoText: 'Select_Team_Channels', + nextAction: data => dispatch(leaveRoom(ERoomType.t, room, data)), + showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_leave')) + }); + } else { + showConfirmationAlert({ + message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), + onPress: () => dispatch(leaveRoom(ERoomType.t, room)) + }); + } } } catch (e) { showConfirmationAlert({ diff --git a/app/views/RoomInfoView/Channel.js b/app/views/RoomInfoView/Channel.tsx similarity index 79% rename from app/views/RoomInfoView/Channel.js rename to app/views/RoomInfoView/Channel.tsx index ea4a584c7..3a1a03442 100644 --- a/app/views/RoomInfoView/Channel.js +++ b/app/views/RoomInfoView/Channel.tsx @@ -1,43 +1,35 @@ import React from 'react'; -import PropTypes from 'prop-types'; import I18n from '../../i18n'; +import { ISubscription } from '../../definitions'; import Item from './Item'; -const Channel = ({ room, theme }) => { +const Channel = ({ room }: { room: ISubscription }) => { const { description, topic, announcement } = room; return ( <> ); }; -Channel.propTypes = { - room: PropTypes.object, - theme: PropTypes.string -}; export default Channel; diff --git a/app/views/RoomInfoView/CustomFields.js b/app/views/RoomInfoView/CustomFields.js deleted file mode 100644 index 777cfdf80..000000000 --- a/app/views/RoomInfoView/CustomFields.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Item from './Item'; - -const CustomFields = ({ customFields, theme }) => { - if (customFields) { - return Object.keys(customFields).map(title => { - if (!customFields[title]) { - return; - } - return ; - }); - } - - return null; -}; -CustomFields.propTypes = { - customFields: PropTypes.object, - theme: PropTypes.string -}; - -export default CustomFields; diff --git a/app/views/RoomInfoView/CustomFields.tsx b/app/views/RoomInfoView/CustomFields.tsx new file mode 100644 index 000000000..0034261bd --- /dev/null +++ b/app/views/RoomInfoView/CustomFields.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import Item from './Item'; + +const CustomFields = ({ customFields }: { customFields: { [key: string]: string } }) => { + if (customFields) { + return ( + <> + {Object.keys(customFields).map((title: string) => { + if (!customFields[title]) { + return null; + } + return ; + })} + + ); + } + + return null; +}; + +export default CustomFields; diff --git a/app/views/RoomInfoView/Direct.js b/app/views/RoomInfoView/Direct.tsx similarity index 60% rename from app/views/RoomInfoView/Direct.js rename to app/views/RoomInfoView/Direct.tsx index cc6d8f393..b9e9e0da7 100644 --- a/app/views/RoomInfoView/Direct.js +++ b/app/views/RoomInfoView/Direct.tsx @@ -1,15 +1,17 @@ import React from 'react'; import { Text, View } from 'react-native'; -import PropTypes from 'prop-types'; import { themes } from '../../constants/colors'; import I18n from '../../i18n'; +import { useTheme } from '../../theme'; import Timezone from './Timezone'; import CustomFields from './CustomFields'; import styles from './styles'; -const Roles = ({ roles, theme }) => - roles && roles.length ? ( +const Roles = ({ roles }: { roles: string[] }) => { + const { theme } = useTheme(); + + if (roles && roles.length) { {I18n.t('Roles')} @@ -21,23 +23,18 @@ const Roles = ({ roles, theme }) => ) : null )} - - ) : null; -Roles.propTypes = { - roles: PropTypes.array, - theme: PropTypes.string + ; + } + + return null; }; -const Direct = ({ roomUser, theme }) => ( +const Direct = ({ roomUser }: { roomUser: any }) => ( <> - - - + + + ); -Direct.propTypes = { - roomUser: PropTypes.object, - theme: PropTypes.string -}; export default Direct; diff --git a/app/views/RoomInfoView/Item.js b/app/views/RoomInfoView/Item.tsx similarity index 67% rename from app/views/RoomInfoView/Item.js rename to app/views/RoomInfoView/Item.tsx index 2b8d19f9d..7296f6542 100644 --- a/app/views/RoomInfoView/Item.js +++ b/app/views/RoomInfoView/Item.tsx @@ -1,25 +1,32 @@ import React from 'react'; import { Text, View } from 'react-native'; -import PropTypes from 'prop-types'; import Markdown from '../../containers/markdown'; import { themes } from '../../constants/colors'; +import { useTheme } from '../../theme'; import styles from './styles'; -const Item = ({ label, content, theme, testID }) => - content ? ( +interface IItem { + label?: string; + content?: string; + testID?: string; +} + +const Item = ({ label, content, testID }: IItem) => { + const { theme } = useTheme(); + + if (!content) { + return null; + } + + return ( {label} - ) : null; -Item.propTypes = { - label: PropTypes.string, - content: PropTypes.string, - theme: PropTypes.string, - testID: PropTypes.string + ); }; export default Item; diff --git a/app/views/RoomInfoView/Livechat.js b/app/views/RoomInfoView/Livechat.tsx similarity index 55% rename from app/views/RoomInfoView/Livechat.js rename to app/views/RoomInfoView/Livechat.tsx index 8635bdddd..ddc630979 100644 --- a/app/views/RoomInfoView/Livechat.js +++ b/app/views/RoomInfoView/Livechat.tsx @@ -1,15 +1,17 @@ import React, { useEffect, useState } from 'react'; import { StyleSheet, Text } from 'react-native'; -import PropTypes from 'prop-types'; import RocketChat from '../../lib/rocketchat'; -import { withTheme } from '../../theme'; +import { useTheme } from '../../theme'; import sharedStyles from '../Styles'; import { themes } from '../../constants/colors'; import I18n from '../../i18n'; +import { ISubscription } from '../../definitions'; +import { ILivechatVisitorModified } from './index'; import CustomFields from './CustomFields'; import Item from './Item'; import Timezone from './Timezone'; +import { ILivechatDepartment } from '../../definitions/ILivechatDepartment'; const styles = StyleSheet.create({ title: { @@ -19,20 +21,19 @@ const styles = StyleSheet.create({ } }); -const Title = ({ title, theme }) => {title}; -Title.propTypes = { - title: PropTypes.string, - theme: PropTypes.string -}; +const Title = ({ title, theme }: { title: string; theme: string }) => ( + {title} +); -const Livechat = ({ room, roomUser, theme }) => { - const [department, setDepartment] = useState({}); +const Livechat = ({ room, roomUser }: { room: ISubscription; roomUser: ILivechatVisitorModified }) => { + const [department, setDepartment] = useState({} as ILivechatDepartment); + const { theme } = useTheme(); - const getDepartment = async id => { + const getDepartment = async (id: string) => { if (id) { const result = await RocketChat.getDepartmentInfo(id); if (result.success) { - setDepartment(result.department); + setDepartment(result.department as ILivechatDepartment); } } }; @@ -50,37 +51,34 @@ const Livechat = ({ room, roomUser, theme }) => { return ( <> - <Timezone utcOffset={roomUser.utc} theme={theme} /> - <Item label={I18n.t('Username')} content={roomUser.username} theme={theme} /> + <Timezone utcOffset={roomUser.utc} /> + <Item label={I18n.t('Username')} content={roomUser.username} /> <Item label={I18n.t('Email')} content={roomUser.visitorEmails?.map(email => email.address).reduce((ret, item) => `${ret}${item}\n`)} - theme={theme} /> <Item label={I18n.t('Phone')} content={roomUser.phone?.map(phone => phone.phoneNumber).reduce((ret, item) => `${ret}${item}\n`)} - theme={theme} /> - <Item label={I18n.t('IP')} content={roomUser.ip} theme={theme} /> - <Item label={I18n.t('OS')} content={roomUser.os} theme={theme} /> - <Item label={I18n.t('Browser')} content={roomUser.browser} theme={theme} /> - <CustomFields customFields={roomUser.livechatData} theme={theme} /> + <Item label={I18n.t('IP')} content={roomUser.ip} /> + <Item label={I18n.t('OS')} content={roomUser.os} /> + <Item label={I18n.t('Browser')} content={roomUser.browser} /> + <CustomFields customFields={roomUser.livechatData} /> <Title title={I18n.t('Conversation')} theme={theme} /> - <Item label={I18n.t('Agent')} content={room.servedBy?.username} theme={theme} /> - <Item label={I18n.t('Facebook')} content={room.facebook?.page.name} theme={theme} /> - <Item label={I18n.t('SMS')} content={room.sms && 'SMS Enabled'} theme={theme} /> - <Item label={I18n.t('Topic')} content={room.topic} theme={theme} /> - <Item label={I18n.t('Tags')} content={room.tags?.join(', ')} theme={theme} /> - <Item label={I18n.t('Department')} content={department.name} theme={theme} /> - <CustomFields customFields={room.livechatData} theme={theme} /> + <Item label={I18n.t('Agent')} content={room.servedBy?.username} /> + {/* TODO: Will be deprecated */} + {/* @ts-ignore */} + <Item label={I18n.t('Facebook')} content={room.facebook?.page.name} /> + {/* TODO: Will be deprecated */} + {/* @ts-ignore */} + <Item label={I18n.t('SMS')} content={room.sms && 'SMS Enabled'} /> + <Item label={I18n.t('Topic')} content={room.topic} /> + <Item label={I18n.t('Tags')} content={room.tags?.join(', ')} /> + <Item label={I18n.t('Department')} content={department.name} /> + <CustomFields customFields={room.livechatData} /> </> ); }; -Livechat.propTypes = { - room: PropTypes.object, - roomUser: PropTypes.object, - theme: PropTypes.string -}; -export default withTheme(Livechat); +export default Livechat; diff --git a/app/views/RoomInfoView/Timezone.js b/app/views/RoomInfoView/Timezone.js deleted file mode 100644 index e40c4c23b..000000000 --- a/app/views/RoomInfoView/Timezone.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import moment from 'moment'; - -import I18n from '../../i18n'; -import Item from './Item'; - -const Timezone = ({ utcOffset, Message_TimeFormat, theme }) => - utcOffset ? ( - <Item - label={I18n.t('Timezone')} - content={`${moment().utcOffset(utcOffset).format(Message_TimeFormat)} (UTC ${utcOffset})`} - theme={theme} - /> - ) : null; -Timezone.propTypes = { - utcOffset: PropTypes.number, - Message_TimeFormat: PropTypes.string, - theme: PropTypes.string -}; - -const mapStateToProps = state => ({ - Message_TimeFormat: state.settings.Message_TimeFormat -}); - -export default connect(mapStateToProps)(Timezone); diff --git a/app/views/RoomInfoView/Timezone.tsx b/app/views/RoomInfoView/Timezone.tsx new file mode 100644 index 000000000..cf2e68f9c --- /dev/null +++ b/app/views/RoomInfoView/Timezone.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import moment from 'moment'; + +import { IApplicationState } from '../../definitions'; +import I18n from '../../i18n'; +import Item from './Item'; +import { TSettingsValues } from '../../reducers/settings'; + +interface ITimezone { + utcOffset?: number; + Message_TimeFormat?: TSettingsValues; +} + +const Timezone = ({ utcOffset, Message_TimeFormat }: ITimezone) => { + if (!utcOffset) { + return null; + } + + return ( + <Item + label={I18n.t('Timezone')} + content={`${moment() + .utcOffset(utcOffset) + .format(Message_TimeFormat as string)} (UTC ${utcOffset})`} + /> + ); +}; + +const mapStateToProps = (state: IApplicationState) => ({ + Message_TimeFormat: state.settings.Message_TimeFormat +}); + +export default connect(mapStateToProps)(Timezone); diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.tsx similarity index 70% rename from app/views/RoomInfoView/index.js rename to app/views/RoomInfoView/index.tsx index 6c677e3cb..1266e5741 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.tsx @@ -1,10 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { ScrollView, Text, View } from 'react-native'; import { BorderlessButton } from 'react-native-gesture-handler'; import { connect } from 'react-redux'; import UAParser from 'ua-parser-js'; import isEmpty from 'lodash/isEmpty'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { CompositeNavigationProp, RouteProp } from '@react-navigation/native'; +import { Observable, Subscription } from 'rxjs'; import { CustomIcon } from '../../lib/Icons'; import Status from '../../containers/Status'; @@ -28,9 +30,22 @@ import Livechat from './Livechat'; import Channel from './Channel'; import Direct from './Direct'; import styles from './styles'; +import { ChatsStackParamList } from '../../stacks/types'; +import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; +import { SubscriptionType, TSubscriptionModel, ISubscription, IUser, IApplicationState } from '../../definitions'; +import { ILivechatVisitor } from '../../definitions/ILivechatVisitor'; -const getRoomTitle = (room, type, name, username, statusText, theme) => - type === 'd' ? ( +interface IGetRoomTitle { + room: ISubscription; + type: SubscriptionType; + name?: string; + username: string; + statusText?: string; + theme: string; +} + +const getRoomTitle = ({ room, type, name, username, statusText, theme }: IGetRoomTitle) => + type === SubscriptionType.DIRECT ? ( <> <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]}> {name} @@ -60,28 +75,57 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => </View> ); -class RoomInfoView extends React.Component { - static propTypes = { - navigation: PropTypes.object, - route: PropTypes.object, - rooms: PropTypes.array, - theme: PropTypes.string, - isMasterDetail: PropTypes.bool, - jitsiEnabled: PropTypes.bool, - editRoomPermission: PropTypes.array, - editOmnichannelContact: PropTypes.array, - editLivechatRoomCustomfields: PropTypes.array, - roles: PropTypes.array - }; +interface IRoomInfoViewProps { + navigation: CompositeNavigationProp< + StackNavigationProp<ChatsStackParamList, 'RoomInfoView'>, + StackNavigationProp<MasterDetailInsideStackParamList> + >; + route: RouteProp<ChatsStackParamList, 'RoomInfoView'>; + rooms: string[]; + theme: string; + isMasterDetail: boolean; + jitsiEnabled: boolean; + editRoomPermission?: string[]; + editOmnichannelContact?: string[]; + editLivechatRoomCustomfields?: string[]; + roles: { [key: string]: string }; +} - constructor(props) { +interface IUserParsed extends IUser { + parsedRoles?: string[]; +} + +export interface ILivechatVisitorModified extends ILivechatVisitor { + os?: string; + browser?: string; +} + +interface IRoomInfoViewState { + room: ISubscription; + // TODO: Could be IUserParsed or ILivechatVisitorModified + roomUser: any; + showEdit: boolean; +} + +class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewState> { + private rid: string; + + private t: SubscriptionType; + + private unsubscribeFocus?: () => void; + + private subscription?: Subscription; + + private roomObservable?: Observable<TSubscriptionModel>; + + constructor(props: IRoomInfoViewProps) { super(props); const room = props.route.params?.room; const roomUser = props.route.params?.member; this.rid = props.route.params?.rid; this.t = props.route.params?.t; this.state = { - room: room || { rid: this.rid, t: this.t }, + room: (room || { rid: this.rid, t: this.t }) as any, roomUser: roomUser || {}, showEdit: false }; @@ -120,14 +164,14 @@ class RoomInfoView extends React.Component { const showCloseModal = route.params?.showCloseModal; navigation.setOptions({ headerLeft: showCloseModal ? () => <HeaderButton.CloseModal navigation={navigation} /> : undefined, - title: t === 'd' ? I18n.t('User_Info') : I18n.t('Room_Info'), + title: t === SubscriptionType.DIRECT ? I18n.t('User_Info') : I18n.t('Room_Info'), headerRight: showEdit ? () => ( <HeaderButton.Container> <HeaderButton.Item iconName='edit' onPress={() => { - const isLivechat = t === 'l'; + const isLivechat = t === SubscriptionType.OMNICHANNEL; logEvent(events[`RI_GO_${isLivechat ? 'LIVECHAT' : 'RI'}_EDIT`]); navigation.navigate(isLivechat ? 'LivechatEditView' : 'RoomInfoEditView', { rid, room, roomUser }); }} @@ -135,21 +179,21 @@ class RoomInfoView extends React.Component { /> </HeaderButton.Container> ) - : null + : undefined }); }; get isDirect() { const { room } = this.state; - return room.t === 'd'; + return room.t === SubscriptionType.DIRECT; } get isLivechat() { const { room } = this.state; - return room.t === 'l'; + return room.t === SubscriptionType.OMNICHANNEL; } - getRoleDescription = id => { + getRoleDescription = (id: string) => { const { roles } = this.props; return roles[id]; }; @@ -157,23 +201,26 @@ class RoomInfoView extends React.Component { loadVisitor = async () => { const { room } = this.state; try { - const result = await RocketChat.getVisitorInfo(room?.visitor?._id); - if (result.success) { - const { visitor } = result; - if (visitor.userAgent) { - const ua = new UAParser(); - ua.setUA(visitor.userAgent); - visitor.os = `${ua.getOS().name} ${ua.getOS().version}`; - visitor.browser = `${ua.getBrowser().name} ${ua.getBrowser().version}`; + if (room.visitor?._id) { + const result = await RocketChat.getVisitorInfo(room.visitor._id); + if (result.success) { + const { visitor } = result; + const params: { os?: string; browser?: string } = {}; + if (visitor.userAgent) { + const ua = new UAParser(); + ua.setUA(visitor.userAgent); + params.os = `${ua.getOS().name} ${ua.getOS().version}`; + params.browser = `${ua.getBrowser().name} ${ua.getBrowser().version}`; + } + this.setState({ roomUser: { ...visitor, ...params } }, () => this.setHeader()); } - this.setState({ roomUser: visitor }, () => this.setHeader()); } } catch (error) { // Do nothing } }; - parseRoles = roleArray => + parseRoles = (roleArray: string[]) => Promise.all( roleArray.map(async role => { const description = await this.getRoleDescription(role); @@ -191,11 +238,12 @@ class RoomInfoView extends React.Component { if (result.success) { const { user } = result; const { roles } = user; + const parsedRoles: { parsedRoles?: string[] } = {}; if (roles && roles.length) { - user.parsedRoles = await this.parseRoles(roles); + parsedRoles.parsedRoles = await this.parseRoles(roles); } - this.setState({ roomUser: user }); + this.setState({ roomUser: { ...user, ...parsedRoles } }); } } catch { // do nothing @@ -218,9 +266,10 @@ class RoomInfoView extends React.Component { loadRoom = async () => { const { room: roomState } = this.state; const { route, editRoomPermission, editOmnichannelContact, editLivechatRoomCustomfields } = this.props; - let room = route.params?.room; - if (room && room.observe) { - this.roomObservable = room.observe(); + let room = route.params?.room as any; + const roomModel = room as TSubscriptionModel; + if (roomModel && roomModel.observe) { + this.roomObservable = roomModel.observe(); this.subscription = this.roomObservable.subscribe(changes => { this.setState({ room: changes }, () => this.setHeader()); }); @@ -245,7 +294,7 @@ class RoomInfoView extends React.Component { }; createDirect = () => - new Promise(async (resolve, reject) => { + new Promise<void>(async (resolve, reject) => { const { route } = this.props; // We don't need to create a direct @@ -309,12 +358,12 @@ class RoomInfoView extends React.Component { RocketChat.callJitsi(room); }; - renderAvatar = (room, roomUser) => { + renderAvatar = (room: ISubscription, roomUser: IUserParsed) => { const { theme } = this.props; return ( <Avatar text={room.name || roomUser.username} style={styles.avatar} type={this.t} size={100} rid={room?.rid}> - {this.t === 'd' && roomUser._id ? ( + {this.t === SubscriptionType.DIRECT && roomUser._id ? ( <View style={[sharedStyles.status, { backgroundColor: themes[theme].auxiliaryBackground }]}> <Status size={20} id={roomUser._id} /> </View> @@ -323,7 +372,7 @@ class RoomInfoView extends React.Component { ); }; - renderButton = (onPress, iconName, text) => { + renderButton = (onPress: () => void, iconName: string, text: string) => { const { theme } = this.props; const onActionPress = async () => { @@ -359,14 +408,15 @@ class RoomInfoView extends React.Component { renderContent = () => { const { room, roomUser } = this.state; - const { theme } = this.props; if (this.isDirect) { - return <Direct roomUser={roomUser} theme={theme} />; - } else if (this.t === 'l') { - return <Livechat room={room} roomUser={roomUser} theme={theme} />; + return <Direct roomUser={roomUser} />; } - return <Channel room={room} theme={theme} />; + + if (this.t === SubscriptionType.OMNICHANNEL) { + return <Livechat room={room} roomUser={roomUser} />; + } + return <Channel room={room} />; }; render() { @@ -379,7 +429,14 @@ class RoomInfoView extends React.Component { <View style={[styles.avatarContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}> {this.renderAvatar(room, roomUser)} <View style={styles.roomTitleContainer}> - {getRoomTitle(room, this.t, roomUser?.name, roomUser?.username, roomUser?.statusText, theme)} + {getRoomTitle({ + room, + type: this.t, + name: roomUser?.name, + username: roomUser?.username, + statusText: roomUser?.statusText, + theme + })} </View> {this.renderButtons()} </View> @@ -390,10 +447,10 @@ class RoomInfoView extends React.Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: IApplicationState) => ({ rooms: state.room.rooms, isMasterDetail: state.app.isMasterDetail, - jitsiEnabled: state.settings.Jitsi_Enabled || false, + jitsiEnabled: (state.settings.Jitsi_Enabled as boolean) || false, editRoomPermission: state.permissions['edit-room'], editOmnichannelContact: state.permissions['edit-omnichannel-contact'], editLivechatRoomCustomfields: state.permissions['edit-livechat-room-customfields'], diff --git a/app/views/RoomInfoView/styles.js b/app/views/RoomInfoView/styles.ts similarity index 100% rename from app/views/RoomInfoView/styles.js rename to app/views/RoomInfoView/styles.ts diff --git a/app/views/RoomMembersView/index.tsx b/app/views/RoomMembersView/index.tsx index ad98dc5df..92d459963 100644 --- a/app/views/RoomMembersView/index.tsx +++ b/app/views/RoomMembersView/index.tsx @@ -216,26 +216,28 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId as string, userId: selectedUser._id }); - if (result.rooms?.length) { - const teamChannels = result.rooms.map((r: any) => ({ - rid: r._id, - name: r.name, - teamId: r.teamId, - alert: r.isLastOwner - })); - navigation.navigate('SelectListView', { - title: 'Remove_Member', - infoText: 'Remove_User_Team_Channels', - data: teamChannels, - nextAction: (selected: any) => this.removeFromTeam(selectedUser, selected), - showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_remove')) - }); - } else { - showConfirmationAlert({ - message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }), - confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }), - onPress: () => this.removeFromTeam(selectedUser) - }); + if (result.success) { + if (result.rooms?.length) { + const teamChannels = result.rooms.map((r: any) => ({ + rid: r._id, + name: r.name, + teamId: r.teamId, + alert: r.isLastOwner + })); + navigation.navigate('SelectListView', { + title: 'Remove_Member', + infoText: 'Remove_User_Team_Channels', + data: teamChannels, + nextAction: (selected: any) => this.removeFromTeam(selectedUser, selected), + showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_remove')) + }); + } else { + showConfirmationAlert({ + message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }), + onPress: () => this.removeFromTeam(selectedUser) + }); + } } } catch (e) { showConfirmationAlert({ diff --git a/app/views/RoomView/LoadMore/LoadMore.stories.js b/app/views/RoomView/LoadMore/LoadMore.stories.js index eb719646b..7f8137d1c 100644 --- a/app/views/RoomView/LoadMore/LoadMore.stories.js +++ b/app/views/RoomView/LoadMore/LoadMore.stories.js @@ -7,11 +7,7 @@ import { longText } from '../../../../storybook/utils'; import { ThemeContext } from '../../../theme'; import { Message, MessageDecorator, StoryProvider } from '../../../../storybook/stories/Message'; import { themes } from '../../../constants/colors'; -import { - MESSAGE_TYPE_LOAD_MORE, - MESSAGE_TYPE_LOAD_NEXT_CHUNK, - MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK -} from '../../../constants/messageTypeLoad'; +import { MessageTypeLoad } from '../../../constants/messageTypeLoad'; import LoadMore from './index'; const stories = storiesOf('LoadMore', module); @@ -23,20 +19,20 @@ stories.add('basic', () => ( <> <LoadMore load={load} /> <LoadMore load={load} runOnRender /> - <LoadMore load={load} type={MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK} /> - <LoadMore load={load} type={MESSAGE_TYPE_LOAD_NEXT_CHUNK} /> + <LoadMore load={load} type={MessageTypeLoad.PREVIOUS_CHUNK} /> + <LoadMore load={load} type={MessageTypeLoad.NEXT_CHUNK} /> </> )); const ThemeStory = ({ theme }) => ( <ThemeContext.Provider value={{ theme }}> <ScrollView style={{ backgroundColor: themes[theme].backgroundColor }}> - <LoadMore load={load} type={MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK} /> + <LoadMore load={load} type={MessageTypeLoad.PREVIOUS_CHUNK} /> <Message msg='Hey!' theme={theme} /> <Message msg={longText} theme={theme} isHeader={false} /> <Message msg='Older message' theme={theme} isHeader={false} /> - <LoadMore load={load} type={MESSAGE_TYPE_LOAD_NEXT_CHUNK} /> - <LoadMore load={load} type={MESSAGE_TYPE_LOAD_MORE} /> + <LoadMore load={load} type={MessageTypeLoad.NEXT_CHUNK} /> + <LoadMore load={load} type={MessageTypeLoad.MORE} /> <Message msg={longText} theme={theme} /> <Message msg='This is the third message' isHeader={false} theme={theme} /> <Message msg='This is the second message' isHeader={false} theme={theme} /> diff --git a/app/views/RoomView/LoadMore/index.tsx b/app/views/RoomView/LoadMore/index.tsx index c29fe2053..7dcd85ea6 100644 --- a/app/views/RoomView/LoadMore/index.tsx +++ b/app/views/RoomView/LoadMore/index.tsx @@ -2,7 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, StyleSheet, Text } from 'react-native'; import { themes } from '../../../constants/colors'; -import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad'; +import { MessageTypeLoad } from '../../../constants/messageTypeLoad'; +import { MessageType } from '../../../definitions'; import { useTheme } from '../../../theme'; import Touch from '../../../utils/touch'; import sharedStyles from '../../Styles'; @@ -20,7 +21,15 @@ const styles = StyleSheet.create({ } }); -const LoadMore = ({ load, type, runOnRender }: { load: Function; type: string; runOnRender: boolean }): React.ReactElement => { +const LoadMore = ({ + load, + type, + runOnRender +}: { + load: Function; + type: MessageType; + runOnRender: boolean; +}): React.ReactElement => { const { theme } = useTheme(); const [loading, setLoading] = useState(false); @@ -43,10 +52,10 @@ const LoadMore = ({ load, type, runOnRender }: { load: Function; type: string; r }, []); let text = 'Load_More'; - if (type === MESSAGE_TYPE_LOAD_NEXT_CHUNK) { + if (type === MessageTypeLoad.NEXT_CHUNK) { text = 'Load_Newer'; } - if (type === MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK) { + if (type === MessageTypeLoad.PREVIOUS_CHUNK) { text = 'Load_Older'; } diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index f5aa59e15..0fbbc4494 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -24,7 +24,7 @@ import I18n from '../../i18n'; import RoomHeader from '../../containers/RoomHeader'; import StatusBar from '../../containers/StatusBar'; import { themes } from '../../constants/colors'; -import { MESSAGE_TYPE_ANY_LOAD, MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad'; +import { MESSAGE_TYPE_ANY_LOAD, MessageTypeLoad } from '../../constants/messageTypeLoad'; import debounce from '../../utils/debounce'; import ReactionsModal from '../../containers/ReactionsModal'; import { LISTENER } from '../../containers/Toast'; @@ -851,7 +851,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> { * if it's from server, we don't have it saved locally and so we fetch surroundings * we test if it's not from threads because we're fetching from threads currently with `getThreadMessages` */ - if (message.fromServer && !message.tmid) { + if (message.fromServer && !message.tmid && this.rid) { await RocketChat.loadSurroundingMessages({ messageId, rid: this.rid }); } // @ts-ignore @@ -1161,12 +1161,12 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> { } let content = null; - if (item.t && MESSAGE_TYPE_ANY_LOAD.includes(item.t)) { + if (item.t && MESSAGE_TYPE_ANY_LOAD.includes(item.t as MessageTypeLoad)) { content = ( <LoadMore load={() => this.onLoadMoreMessages(item)} type={item.t} - runOnRender={item.t === MESSAGE_TYPE_LOAD_MORE && !previousItem} + runOnRender={item.t === MessageTypeLoad.MORE && !previousItem} /> ); } else { diff --git a/app/views/RoomView/services/getMoreMessages.ts b/app/views/RoomView/services/getMoreMessages.ts index dae63c3b0..99e22bbc4 100644 --- a/app/views/RoomView/services/getMoreMessages.ts +++ b/app/views/RoomView/services/getMoreMessages.ts @@ -1,11 +1,7 @@ -import { MessageType, SubscriptionType, TAnyMessageModel } from '../../../definitions'; +import { SubscriptionType, TAnyMessageModel } from '../../../definitions'; import loadMessagesForRoom from '../../../lib/methods/loadMessagesForRoom'; import loadNextMessages from '../../../lib/methods/loadNextMessages'; -import { - MESSAGE_TYPE_LOAD_MORE, - MESSAGE_TYPE_LOAD_NEXT_CHUNK, - MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK -} from '../../../constants/messageTypeLoad'; +import { MessageTypeLoad } from '../../../constants/messageTypeLoad'; const getMoreMessages = ({ rid, @@ -18,7 +14,7 @@ const getMoreMessages = ({ tmid?: string; loaderItem: TAnyMessageModel; }): Promise<void> => { - if ([MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK].includes(loaderItem.t as MessageType)) { + if ([MessageTypeLoad.MORE, MessageTypeLoad.PREVIOUS_CHUNK].includes(loaderItem.t as MessageTypeLoad)) { return loadMessagesForRoom({ rid, t: t as any, @@ -27,7 +23,7 @@ const getMoreMessages = ({ }); } - if (loaderItem.t === MESSAGE_TYPE_LOAD_NEXT_CHUNK) { + if (loaderItem.t === MessageTypeLoad.NEXT_CHUNK) { return loadNextMessages({ rid, tmid, diff --git a/app/views/ThreadMessagesView/index.tsx b/app/views/ThreadMessagesView/index.tsx index d2ea4527d..cf40dab82 100644 --- a/app/views/ThreadMessagesView/index.tsx +++ b/app/views/ThreadMessagesView/index.tsx @@ -7,7 +7,6 @@ import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { RouteProp } from '@react-navigation/native'; import { Observable, Subscription } from 'rxjs'; -import Database from '@nozbe/watermelondb/Database'; import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; @@ -33,13 +32,12 @@ import EventEmitter from '../../utils/events'; import { LISTENER } from '../../containers/Toast'; import SearchHeader from '../../containers/SearchHeader'; import { ChatsStackParamList } from '../../stacks/types'; -import { IThreadResult, TThreadModel } from '../../definitions/IThread'; import { Filter } from './filters'; import DropdownItemHeader from './Dropdown/DropdownItemHeader'; import Dropdown from './Dropdown'; import Item from './Item'; import styles from './styles'; -import { SubscriptionType, TSubscriptionModel } from '../../definitions/ISubscription'; +import { IMessage, SubscriptionType, TSubscriptionModel, TThreadModel } from '../../definitions'; const API_FETCH_COUNT = 50; @@ -259,8 +257,8 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre remove, lastThreadSync }: { - update: IThreadResult[]; - remove?: IThreadResult[]; + update: IMessage[]; + remove?: IMessage[]; lastThreadSync: Date; }) => { const { subscription } = this.state; @@ -272,11 +270,9 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre } try { - const db: Database = database.active; + const db = database.active; const threadsCollection = db.get('threads'); - // TODO: Refactor when migrate room - // @ts-ignore - const allThreadsRecords = (await subscription.threads.fetch()) as TThreadModel[]; + const allThreadsRecords = await subscription.threads.fetch(); let threadsToCreate: any[] = []; let threadsToUpdate: any[] = []; let threadsToDelete: any[] = []; @@ -287,7 +283,7 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre } if (update && update.length) { - update = update.map(m => buildMessage(m)) as IThreadResult[]; + update = update.map(m => buildMessage(m)) as IMessage[]; // filter threads threadsToCreate = update.filter(i1 => !allThreadsRecords.find((i2: { id: string }) => i1._id === i2.id)); threadsToUpdate = allThreadsRecords.filter((i1: { id: string }) => update.find(i2 => i1.id === i2._id)); diff --git a/app/views/UserNotificationPreferencesView/index.tsx b/app/views/UserNotificationPreferencesView/index.tsx index 8961cb38f..79a681732 100644 --- a/app/views/UserNotificationPreferencesView/index.tsx +++ b/app/views/UserNotificationPreferencesView/index.tsx @@ -106,10 +106,12 @@ class UserNotificationPreferencesView extends React.Component< const { user } = this.props; const { id } = user; const result = await RocketChat.setUserPreferences(id, params); - const { - user: { settings } - } = result; - this.setState({ preferences: settings.preferences }); + if (result.success) { + const { + user: { settings } + } = result; + this.setState({ preferences: settings.preferences }); + } }; render() { diff --git a/package.json b/package.json index f3fc666cc..5d7325a8a 100644 --- a/package.json +++ b/package.json @@ -143,8 +143,8 @@ "@rocket.chat/eslint-config": "^0.4.0", "@storybook/addon-storyshots": "5.3.21", "@storybook/react-native": "5.3.25", - "@types/bytebuffer": "^5.0.43", "@testing-library/react-native": "^9.0.0", + "@types/bytebuffer": "^5.0.43", "@types/ejson": "^2.1.3", "@types/jest": "^26.0.24", "@types/lodash": "^4.14.171", @@ -157,6 +157,7 @@ "@types/react-redux": "^7.1.18", "@types/react-test-renderer": "^17.0.1", "@types/semver": "^7.3.9", + "@types/ua-parser-js": "^0.7.36", "@types/url-parse": "^1.4.6", "@typescript-eslint/eslint-plugin": "^4.28.3", "@typescript-eslint/parser": "^4.28.5", @@ -245,13 +246,13 @@ } } }, - "and.emu.debug": { + "android.emu.debug": { "device": "Pixel_API_28_AOSP", "type": "android.emulator", "binaryPath": "android/app/build/outputs/apk/e2ePlay/debug/app-e2e-play-debug.apk", "build": "cd android && ./gradlew app:assembleE2ePlayDebug app:assembleE2ePlayDebugAndroidTest -DtestBuildType=debug && cd .." }, - "and.emu.release": { + "android.emu.release": { "device": "Pixel_API_28_AOSP", "type": "android.emulator", "binaryPath": "android/app/build/outputs/apk/e2ePlay/release/app-e2e-play-release.apk", diff --git a/yarn.lock b/yarn.lock index 69ec7d91c..8b576f36e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4534,6 +4534,11 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02" integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ== +"@types/ua-parser-js@^0.7.36": + version "0.7.36" + resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190" + integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== + "@types/uglify-js@*": version "3.9.2" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.2.tgz#01992579debba674e1e359cd6bcb1a1d0ab2e02b"