Merge branch 'develop' into chore.dehydrate-login-methods-from-rocketchatjs

This commit is contained in:
Gerzon Z 2022-03-02 12:43:29 -04:00 committed by GitHub
commit f8fe01bf6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 1258 additions and 973 deletions

View File

@ -144,7 +144,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.25.0" versionName "4.26.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]

View File

@ -4,7 +4,7 @@ import { ERoomType } from '../definitions/ERoomType';
import { ROOM } from './actionsTypes'; import { ROOM } from './actionsTypes';
// TYPE RETURN RELATED // TYPE RETURN RELATED
type ISelected = Record<string, string>; type ISelected = string[];
export interface ITransferData { export interface ITransferData {
roomId: string; roomId: string;

View File

@ -1,26 +1,26 @@
import { Action } from 'redux'; import { Action } from 'redux';
import { ISettings, TSettings } from '../reducers/settings'; import { TSettingsState, TSupportedSettings, TSettingsValues } from '../reducers/settings';
import { SETTINGS } from './actionsTypes'; import { SETTINGS } from './actionsTypes';
interface IAddSettings extends Action { interface IAddSettings extends Action {
payload: ISettings; payload: TSettingsState;
} }
interface IUpdateSettings extends Action { interface IUpdateSettings extends Action {
payload: { id: string; value: TSettings }; payload: { id: TSupportedSettings; value: TSettingsValues };
} }
export type IActionSettings = IAddSettings & IUpdateSettings; export type IActionSettings = IAddSettings & IUpdateSettings;
export function addSettings(settings: ISettings): IAddSettings { export function addSettings(settings: TSettingsState): IAddSettings {
return { return {
type: SETTINGS.ADD, type: SETTINGS.ADD,
payload: settings payload: settings
}; };
} }
export function updateSettings(id: string, value: TSettings): IUpdateSettings { export function updateSettings(id: TSupportedSettings, value: TSettingsValues): IUpdateSettings {
return { return {
type: SETTINGS.UPDATE, type: SETTINGS.UPDATE,
payload: { id, value } payload: { id, value }

View File

@ -206,4 +206,4 @@ export default {
Canned_Responses_Enable: { Canned_Responses_Enable: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
} }
}; } as const;

View File

@ -15,19 +15,12 @@ import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet'; import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header'; import Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events'; import events from '../../utils/log/events';
import { TMessageModel } from '../../definitions/IMessage'; import { ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
interface IMessageActions { export interface IMessageActions {
room: { room: TSubscriptionModel;
rid: string; tmid?: string;
autoTranslateLanguage: any; user: Pick<ILoggedUser, 'id'>;
autoTranslate: any;
reactWhenReadOnly: any;
};
tmid: string;
user: {
id: string | number;
};
editInit: Function; editInit: Function;
reactionInit: Function; reactionInit: Function;
onReactionPress: Function; onReactionPress: Function;
@ -270,8 +263,11 @@ const MessageActions = React.memo(
} }
}; };
const handleToggleTranslation = async (message: TMessageModel) => { const handleToggleTranslation = async (message: TAnyMessageModel) => {
try { try {
if (!room.autoTranslateLanguage) {
return;
}
const db = database.active; const db = database.active;
await db.write(async () => { await db.write(async () => {
await message.update(m => { await message.update(m => {
@ -321,7 +317,7 @@ const MessageActions = React.memo(
}); });
}; };
const getOptions = (message: TMessageModel) => { const getOptions = (message: TAnyMessageModel) => {
let options: any = []; let options: any = [];
// Reply // Reply
@ -447,7 +443,7 @@ const MessageActions = React.memo(
return options; return options;
}; };
const showMessageActions = async (message: TMessageModel) => { const showMessageActions = async (message: TAnyMessageModel) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS); logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions(); await getPermissions();
showActionSheet({ showActionSheet({

View File

@ -73,7 +73,7 @@ const videoPickerConfig = {
mediaType: 'video' mediaType: 'video'
}; };
interface IMessageBoxProps { export interface IMessageBoxProps {
rid: string; rid: string;
baseUrl: string; baseUrl: string;
message: IMessage; message: IMessage;

View File

@ -10,6 +10,7 @@ import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { TGetCustomEmoji } from '../definitions/IEmoji'; import { TGetCustomEmoji } from '../definitions/IEmoji';
import { TMessageModel, ILoggedUser } from '../definitions';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -65,23 +66,25 @@ interface IItem {
usernames: any; usernames: any;
emoji: string; emoji: string;
}; };
user?: { username: any }; user?: Pick<ILoggedUser, 'username'>;
baseUrl?: string; baseUrl?: string;
getCustomEmoji?: TGetCustomEmoji; getCustomEmoji?: TGetCustomEmoji;
theme?: string; theme?: string;
} }
interface IModalContent { interface IModalContent {
message?: { message?: TMessageModel;
reactions: any;
};
onClose: Function; onClose: Function;
theme: string; theme: string;
} }
interface IReactionsModal { interface IReactionsModal {
message?: any;
user?: Pick<ILoggedUser, 'username'>;
isVisible: boolean; isVisible: boolean;
onClose(): void; onClose(): void;
baseUrl: string;
getCustomEmoji?: TGetCustomEmoji;
theme: string; theme: string;
} }

View File

@ -52,9 +52,9 @@ interface IThreadDetails {
const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => { const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => {
const { theme } = useTheme(); const { theme } = useTheme();
let { tcount } = item; let count: string | number | undefined = item.tcount;
if (tcount && tcount >= 1000) { if (count && count >= 1000) {
tcount = '+999'; count = '+999';
} }
let replies: number | string = item?.replies?.length ?? 0; let replies: number | string = item?.replies?.length ?? 0;
@ -70,7 +70,7 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT
<View style={styles.detailContainer}> <View style={styles.detailContainer}>
<CustomIcon name='threads' size={24} color={themes[theme!].auxiliaryText} /> <CustomIcon name='threads' size={24} color={themes[theme!].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
{tcount} {count}
</Text> </Text>
</View> </View>

View File

@ -87,7 +87,7 @@ const Message = React.memo((props: IMessage) => {
<View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}> <View style={[styles.messageContent, props.isHeader && styles.messageContentWithHeader]}>
<MessageInner {...props} /> <MessageInner {...props} />
</View> </View>
<ReadReceipt isReadReceiptEnabled={props.isReadReceiptEnabled} unread={props.unread} theme={props.theme} /> <ReadReceipt isReadReceiptEnabled={props.isReadReceiptEnabled} unread={props.unread || false} theme={props.theme} />
</View> </View>
</View> </View>
); );

View File

@ -94,7 +94,7 @@ const styles = StyleSheet.create({
interface IMessageTitle { interface IMessageTitle {
attachment: IAttachment; attachment: IAttachment;
timeFormat: string; timeFormat?: string;
theme: string; theme: string;
} }
@ -112,7 +112,7 @@ interface IMessageFields {
interface IMessageReply { interface IMessageReply {
attachment: IAttachment; attachment: IAttachment;
timeFormat: string; timeFormat?: string;
index: number; index: number;
theme: string; theme: string;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;

View File

@ -40,7 +40,7 @@ const styles = StyleSheet.create({
interface IMessageUser { interface IMessageUser {
isHeader?: boolean; isHeader?: boolean;
hasError?: boolean; hasError?: boolean;
useRealName: boolean; useRealName?: boolean;
author?: { author?: {
_id: string; _id: string;
name?: string; name?: string;
@ -50,25 +50,26 @@ interface IMessageUser {
ts?: Date; ts?: Date;
timeFormat?: string; timeFormat?: string;
theme: string; theme: string;
navToRoomInfo: Function; navToRoomInfo?: Function;
type: string; type: string;
} }
const User = React.memo( const User = React.memo(
({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, type, ...props }: IMessageUser) => { ({ isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, type, ...props }: IMessageUser) => {
if (isHeader || hasError) { if (isHeader || hasError) {
const navParam = {
t: 'd',
rid: author!._id
};
const { user } = useContext(MessageContext); const { user } = useContext(MessageContext);
const username = (useRealName && author!.name) || author!.username; const username = (useRealName && author?.name) || author?.username;
const aliasUsername = alias ? ( const aliasUsername = alias ? (
<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text> <Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>
) : null; ) : null;
const time = moment(ts).format(timeFormat); const time = moment(ts).format(timeFormat);
const onUserPress = () => navToRoomInfo(navParam); const onUserPress = () => {
const isDisabled = author!._id === user.id; navToRoomInfo?.({
t: 'd',
rid: author?._id
});
};
const isDisabled = author?._id === user.id;
const textContent = ( const textContent = (
<> <>

View File

@ -10,9 +10,10 @@ import messagesStatus from '../../constants/messagesStatus';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { TGetCustomEmoji } from '../../definitions/IEmoji'; import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { TAnyMessageModel } from '../../definitions';
interface IMessageContainerProps { interface IMessageContainerProps {
item: any; item: TAnyMessageModel;
user: { user: {
id: string; id: string;
username: string; username: string;
@ -20,24 +21,16 @@ interface IMessageContainerProps {
}; };
msg?: string; msg?: string;
rid?: string; rid?: string;
timeFormat: string; timeFormat?: string;
style?: ViewStyle; style?: ViewStyle;
archived?: boolean; archived?: boolean;
broadcast?: boolean; broadcast?: boolean;
previousItem?: { previousItem?: TAnyMessageModel;
ts: any;
u: any;
groupable: any;
id: string;
tmid: string;
status: any;
};
isHeader: boolean;
baseUrl: string; baseUrl: string;
Message_GroupingPeriod?: number; Message_GroupingPeriod?: number;
isReadReceiptEnabled?: boolean; isReadReceiptEnabled?: boolean;
isThreadRoom: boolean; isThreadRoom: boolean;
useRealName: boolean; useRealName?: boolean;
autoTranslateRoom?: boolean; autoTranslateRoom?: boolean;
autoTranslateLanguage?: string; autoTranslateLanguage?: string;
status?: number; status?: number;
@ -59,11 +52,11 @@ interface IMessageContainerProps {
callJitsi?: Function; callJitsi?: Function;
blockAction?: Function; blockAction?: Function;
onAnswerButtonPress?: Function; onAnswerButtonPress?: Function;
theme: string; theme?: string;
threadBadgeColor?: string; threadBadgeColor?: string;
toggleFollowThread?: Function; toggleFollowThread?: Function;
jumpToMessage?: Function; jumpToMessage?: Function;
onPress: Function; onPress?: Function;
} }
class MessageContainer extends React.Component<IMessageContainerProps> { class MessageContainer extends React.Component<IMessageContainerProps> {
@ -222,7 +215,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
this.setState({ isManualUnignored: true }); this.setState({ isManualUnignored: true });
}; };
get isHeader() { get isHeader(): boolean {
const { item, previousItem, broadcast, Message_GroupingPeriod } = this.props; const { item, previousItem, broadcast, Message_GroupingPeriod } = this.props;
if (this.hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) { if (this.hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) {
return true; return true;
@ -230,9 +223,11 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
try { try {
if ( if (
previousItem && previousItem &&
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense
previousItem.ts.toDateString() === item.ts.toDateString() && previousItem.ts.toDateString() === item.ts.toDateString() &&
previousItem.u.username === item.u.username && previousItem.u.username === item.u.username &&
!(previousItem.groupable === false || item.groupable === false || broadcast === true) && !(previousItem.groupable === false || item.groupable === false || broadcast === true) &&
// @ts-ignore TODO: IMessage vs IMessageFromServer non-sense
item.ts - previousItem.ts < Message_GroupingPeriod! * 1000 && item.ts - previousItem.ts < Message_GroupingPeriod! * 1000 &&
previousItem.tmid === item.tmid previousItem.tmid === item.tmid
) { ) {
@ -244,7 +239,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
} }
} }
get isThreadReply() { get isThreadReply(): boolean {
const { item, previousItem, isThreadRoom } = this.props; const { item, previousItem, isThreadRoom } = this.props;
if (isThreadRoom) { if (isThreadRoom) {
return false; return false;
@ -255,37 +250,40 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
return false; return false;
} }
get isThreadSequential() { get isThreadSequential(): boolean {
const { item, isThreadRoom } = this.props; const { item, isThreadRoom } = this.props;
if (isThreadRoom) { if (isThreadRoom) {
return false; return false;
} }
return item.tmid; return !!item.tmid;
} }
get isEncrypted() { get isEncrypted(): boolean {
const { item } = this.props; const { item } = this.props;
const { t, e2e } = item; const { t, e2e } = item;
return t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE; return t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE;
} }
get isInfo() { get isInfo(): boolean {
const { item } = this.props; const { item } = this.props;
return SYSTEM_MESSAGES.includes(item.t); return (item.t && SYSTEM_MESSAGES.includes(item.t)) ?? false;
} }
get isTemp() { get isTemp(): boolean {
const { item } = this.props; const { item } = this.props;
return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR; return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR;
} }
get isIgnored() { get isIgnored(): boolean {
const { isManualUnignored } = this.state; const { isManualUnignored } = this.state;
const { isIgnored } = this.props; const { isIgnored } = this.props;
return isManualUnignored ? false : isIgnored; if (isManualUnignored) {
return false;
}
return isIgnored ?? false;
} }
get hasError() { get hasError(): boolean {
const { item } = this.props; const { item } = this.props;
return item.status === messagesStatus.ERROR; return item.status === messagesStatus.ERROR;
} }
@ -405,13 +403,12 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
rid={rid!} rid={rid!}
author={u} author={u}
ts={ts} ts={ts}
type={t} type={t as any}
attachments={attachments} attachments={attachments}
blocks={blocks} blocks={blocks}
urls={urls} urls={urls}
reactions={reactions} reactions={reactions}
alias={alias} alias={alias}
/* @ts-ignore*/
avatar={avatar} avatar={avatar}
emoji={emoji} emoji={emoji}
timeFormat={timeFormat} timeFormat={timeFormat}
@ -424,16 +421,19 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
role={role} role={role}
drid={drid} drid={drid}
dcount={dcount} dcount={dcount}
// @ts-ignore
dlm={dlm} dlm={dlm}
tmid={tmid} tmid={tmid}
tcount={tcount} tcount={tcount}
// @ts-ignore
tlm={tlm} tlm={tlm}
tmsg={tmsg} tmsg={tmsg}
fetchThreadName={fetchThreadName!} fetchThreadName={fetchThreadName!}
// @ts-ignore
mentions={mentions} mentions={mentions}
channels={channels} channels={channels}
isIgnored={this.isIgnored!} isIgnored={this.isIgnored}
isEdited={editedBy && !!editedBy.username} isEdited={(editedBy && !!editedBy.username) ?? false}
isHeader={this.isHeader} isHeader={this.isHeader}
isThreadReply={this.isThreadReply} isThreadReply={this.isThreadReply}
isThreadSequential={this.isThreadSequential} isThreadSequential={this.isThreadSequential}
@ -447,7 +447,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
navToRoomInfo={navToRoomInfo!} navToRoomInfo={navToRoomInfo!}
callJitsi={callJitsi!} callJitsi={callJitsi!}
blockAction={blockAction!} blockAction={blockAction!}
theme={theme} theme={theme as string}
highlighted={highlighted!} highlighted={highlighted!}
/> />
</MessageContext.Provider> </MessageContext.Provider>

View File

@ -7,7 +7,7 @@ export type TMessageType = 'discussion-created' | 'jitsi_call_started';
export interface IMessageAttachments { export interface IMessageAttachments {
attachments: any; attachments: any;
timeFormat: string; timeFormat?: string;
showAttachment: Function; showAttachment: Function;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
@ -66,26 +66,26 @@ export interface IMessageContent {
_id: string; _id: string;
isTemp: boolean; isTemp: boolean;
isInfo: boolean; isInfo: boolean;
tmid: string; tmid?: string;
isThreadRoom: boolean; isThreadRoom: boolean;
msg: string; msg?: string;
md: MarkdownAST; md?: MarkdownAST;
theme: string; theme: string;
isEdited: boolean; isEdited: boolean;
isEncrypted: boolean; isEncrypted: boolean;
getCustomEmoji: TGetCustomEmoji; getCustomEmoji: TGetCustomEmoji;
channels: IUserChannel[]; channels?: IUserChannel[];
mentions: IUserMention[]; mentions?: IUserMention[];
navToRoomInfo: Function; navToRoomInfo?: Function;
useRealName: boolean; useRealName?: boolean;
isIgnored: boolean; isIgnored: boolean;
type: string; type: string;
} }
export interface IMessageDiscussion { export interface IMessageDiscussion {
msg: string; msg?: string;
dcount: number; dcount?: number;
dlm: Date; dlm?: Date;
theme: string; theme: string;
} }
@ -98,10 +98,10 @@ export interface IMessageEmoji {
} }
export interface IMessageThread { export interface IMessageThread {
msg: string; msg?: string;
tcount: number; tcount?: number;
theme: string; theme: string;
tlm: Date; tlm?: Date;
isThreadRoom: boolean; isThreadRoom: boolean;
id: string; id: string;
} }
@ -123,8 +123,8 @@ export interface IMessageTouchable {
} }
export interface IMessageRepliedThread { export interface IMessageRepliedThread {
tmid: string; tmid?: string;
tmsg: string; tmsg?: string;
id: string; id: string;
isHeader: boolean; isHeader: boolean;
theme: string; theme: string;
@ -154,7 +154,7 @@ export interface IMessage extends IMessageRepliedThread, IMessageInner {
style: any; style: any;
onLongPress: Function; onLongPress: Function;
isReadReceiptEnabled: boolean; isReadReceiptEnabled: boolean;
unread: boolean; unread?: boolean;
theme: string; theme: string;
isIgnored: boolean; isIgnored: boolean;
} }

View File

@ -2,9 +2,12 @@ import { TMessageModel } from '../../definitions/IMessage';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DISCUSSION } from './constants'; import { DISCUSSION } from './constants';
export const formatMessageCount = (count: number, type: string) => { export const formatMessageCount = (count?: number, type?: string): string => {
const discussion = type === DISCUSSION; const discussion = type === DISCUSSION;
let text = discussion ? I18n.t('No_messages_yet') : null; let text = discussion ? I18n.t('No_messages_yet') : null;
if (!count) {
return text;
}
if (count === 1) { if (count === 1) {
text = `${count} ${discussion ? I18n.t('message') : I18n.t('reply')}`; text = `${count} ${discussion ? I18n.t('message') : I18n.t('reply')}`;
} else if (count > 1 && count < 1000) { } else if (count > 1 && count < 1000) {

View File

@ -21,4 +21,10 @@ export interface IEmojiCategory {
tabLabel: string; tabLabel: string;
} }
export type TGetCustomEmoji = (name: string) => IEmoji | null; // TODO: copied from reducers/customEmojis. We can unify later.
export interface IReduxEmoji {
name: string;
extension: any;
}
export type TGetCustomEmoji = (name: string) => any;

View File

@ -3,9 +3,25 @@ import { MarkdownAST } from '@rocket.chat/message-parser';
import { IAttachment } from './IAttachment'; import { IAttachment } from './IAttachment';
import { IReaction } from './IReaction'; 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'; import { IUrlFromServer } from './IUrl';
export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj'; 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 interface IUserMessage { export interface IUserMessage {
_id: string; _id: string;
@ -139,4 +155,5 @@ export interface IMessage extends IMessageFromServer {
export type TMessageModel = IMessage & Model; export type TMessageModel = IMessage & Model;
export type TAnyMessageModel = TMessageModel | TThreadModel | TThreadMessageModel;
export type TTypeMessages = IMessageFromServer | ILoadMoreMessage | IMessage; export type TTypeMessages = IMessageFromServer | ILoadMoreMessage | IMessage;

View File

@ -12,7 +12,7 @@ export enum SubscriptionType {
DIRECT = 'd', DIRECT = 'd',
CHANNEL = 'c', CHANNEL = 'c',
OMNICHANNEL = 'l', OMNICHANNEL = 'l',
E2E = 'e2e', E2E = 'e2e', // FIXME: this is not a type of subscription
THREAD = 'thread' // FIXME: this is not a type of subscription THREAD = 'thread' // FIXME: this is not a type of subscription
} }
@ -36,7 +36,7 @@ export interface ISubscription {
_updatedAt?: string; // from server _updatedAt?: string; // from server
v?: IVisitor; v?: IVisitor;
f: boolean; f: boolean;
t: SubscriptionType; t: string; // TODO: we need to review this type later
ts: string | Date; ts: string | Date;
ls: Date; ls: Date;
name: string; name: string;
@ -73,14 +73,15 @@ export interface ISubscription {
lastThreadSync?: Date; lastThreadSync?: Date;
jitsiTimeout?: number; jitsiTimeout?: number;
autoTranslate?: boolean; autoTranslate?: boolean;
autoTranslateLanguage: string; autoTranslateLanguage?: string;
lastMessage?: ILastMessage; lastMessage?: ILastMessage; // TODO: we need to use IMessage here
hideUnreadStatus?: boolean; hideUnreadStatus?: boolean;
sysMes?: string[] | boolean; sysMes?: string[] | boolean;
uids?: string[]; uids?: string[];
usernames?: string[]; usernames?: string[];
visitor?: IVisitor; visitor?: IVisitor;
departmentId?: string; departmentId?: string;
status?: string;
servedBy?: IServedBy; servedBy?: IServedBy;
livechatData?: any; livechatData?: any;
tags?: string[]; tags?: string[];

View File

@ -2,8 +2,7 @@ import Model from '@nozbe/watermelondb/Model';
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import { IAttachment } from './IAttachment'; import { IAttachment } from './IAttachment';
import { IEditedBy, IUserChannel, IUserMention, IUserMessage, MessageType } from './IMessage'; import { IMessage, IUserChannel, IUserMention, IUserMessage } from './IMessage';
import { IReaction } from './IReaction';
import { IUrl } from './IUrl'; import { IUrl } from './IUrl';
interface IFileThread { interface IFileThread {
@ -34,42 +33,9 @@ export interface IThreadResult {
tlm?: string | Date; tlm?: string | Date;
} }
export interface IThread { export interface IThread extends IMessage {
id: string;
tmsg?: string; tmsg?: string;
msg?: string; draftMessage?: string;
t?: MessageType;
rid: string;
_updatedAt?: string | Date;
ts?: string | Date;
u?: IUserMessage;
alias?: string;
parseUrls?: boolean;
groupable?: boolean;
avatar?: string;
emoji?: string;
attachments?: IAttachment[];
urls?: IUrl[];
status?: number;
pinned?: boolean;
starred?: boolean;
editedBy?: IEditedBy;
reactions?: IReaction[];
role?: string;
drid?: string;
dcount?: number | string;
dlm?: string | Date;
tmid?: string;
tcount?: number | string;
tlm?: string | Date;
replies?: string[];
mentions?: IUserMention[];
channels?: IUserChannel[];
unread?: boolean;
autoTranslate?: boolean;
translations?: any;
e2e?: string;
subscription?: { id: string };
} }
export type TThreadModel = IThread & Model; export type TThreadModel = IThread & Model;

View File

@ -1,47 +1,9 @@
import Model from '@nozbe/watermelondb/Model'; import Model from '@nozbe/watermelondb/Model';
import { IAttachment } from './IAttachment'; import { IMessage } from './IMessage';
import { IEditedBy, ITranslations, IUserChannel, IUserMention, IUserMessage, MessageType } from './IMessage';
import { IReaction } from './IReaction';
import { IUrl } from './IUrl';
export interface IThreadMessage { export interface IThreadMessage extends IMessage {
id: string;
_id: string;
tmsg?: string; tmsg?: string;
msg?: string;
t?: MessageType;
rid: string;
ts: string | Date;
u: IUserMessage;
alias?: string;
parseUrls?: boolean;
groupable?: boolean;
avatar?: string;
emoji?: string;
attachments?: IAttachment[];
urls?: IUrl[];
_updatedAt?: string | Date;
status?: number;
pinned?: boolean;
starred?: boolean;
editedBy?: IEditedBy;
reactions?: IReaction[];
role?: string;
drid?: string;
dcount?: number;
dlm?: string | Date;
tmid?: string;
tcount?: number;
tlm?: string | Date;
replies?: string[];
mentions?: IUserMention[];
channels?: IUserChannel[];
unread?: boolean;
autoTranslate?: boolean;
translations?: ITranslations[];
e2e?: string;
subscription?: { id: string };
} }
export type TThreadMessageModel = IThreadMessage & Model; export type TThreadMessageModel = IThreadMessage & Model;

View File

@ -31,6 +31,7 @@ export interface IBaseScreen<T extends Record<string, object | undefined>, S ext
route: RouteProp<T, S>; route: RouteProp<T, S>;
dispatch: Dispatch; dispatch: Dispatch;
theme: string; theme: string;
isMasterDetail: boolean;
} }
export * from './redux'; export * from './redux';

View File

@ -28,15 +28,15 @@ import { IRoles } from '../../reducers/roles';
import { IRoom } from '../../reducers/room'; import { IRoom } from '../../reducers/room';
import { ISelectedUsers } from '../../reducers/selectedUsers'; import { ISelectedUsers } from '../../reducers/selectedUsers';
import { IServer } from '../../reducers/server'; import { IServer } from '../../reducers/server';
import { ISettings } from '../../reducers/settings'; import { TSettingsState } from '../../reducers/settings';
import { IShare } from '../../reducers/share'; import { IShare } from '../../reducers/share';
import { IPermissionsState } from '../../reducers/permissions'; import { IPermissionsState } from '../../reducers/permissions';
import { IEnterpriseModules } from '../../reducers/enterpriseModules'; import { IEnterpriseModules } from '../../reducers/enterpriseModules';
export interface IApplicationState { export interface IApplicationState {
settings: ISettings; settings: TSettingsState;
meteor: IConnect;
login: ILogin; login: ILogin;
meteor: IConnect;
server: IServer; server: IServer;
selectedUsers: ISelectedUsers; selectedUsers: ISelectedUsers;
app: IApp; app: IApp;

View File

@ -75,4 +75,6 @@ export default class Thread extends Model {
@json('translations', sanitizer) translations; @json('translations', sanitizer) translations;
@field('e2e') e2e; @field('e2e') e2e;
@field('draft_message') draftMessage;
} }

View File

@ -199,6 +199,15 @@ export default schemaMigrations({
columns: [{ name: 'md', type: 'string', isOptional: true }] columns: [{ name: 'md', type: 'string', isOptional: true }]
}) })
] ]
},
{
toVersion: 15,
steps: [
addColumns({
table: 'threads',
columns: [{ name: 'draft_message', type: 'string', isOptional: true }]
})
]
} }
] ]
}); });

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 14, version: 15,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'subscriptions', name: 'subscriptions',
@ -153,7 +153,8 @@ export default appSchema({
{ name: 'unread', type: 'boolean', isOptional: true }, { name: 'unread', type: 'boolean', isOptional: true },
{ name: 'auto_translate', type: 'boolean', isOptional: true }, { name: 'auto_translate', type: 'boolean', isOptional: true },
{ name: 'translations', type: 'string', isOptional: true }, { name: 'translations', type: 'string', isOptional: true },
{ name: 'e2e', type: 'string', isOptional: true } { name: 'e2e', type: 'string', isOptional: true },
{ name: 'draft_message', type: 'string', isOptional: true }
] ]
}), }),
tableSchema({ tableSchema({

View File

@ -10,7 +10,7 @@ export const E2E_BANNER_TYPE = {
REQUEST_PASSWORD: 'REQUEST_PASSWORD', REQUEST_PASSWORD: 'REQUEST_PASSWORD',
SAVE_PASSWORD: 'SAVE_PASSWORD' SAVE_PASSWORD: 'SAVE_PASSWORD'
}; };
export const E2E_ROOM_TYPES = { export const E2E_ROOM_TYPES: Record<string, string> = {
d: 'd', d: 'd',
p: 'p' p: 'p'
}; };

View File

@ -43,7 +43,7 @@ async function open({ type, rid, name }: { type: ERoomTypes; rid: string; name:
if ((type === ERoomTypes.CHANNEL || type === ERoomTypes.GROUP) && !rid) { if ((type === ERoomTypes.CHANNEL || type === ERoomTypes.GROUP) && !rid) {
// RC 0.72.0 // RC 0.72.0
// @ts-ignore // @ts-ignore
const result: any = await sdk.get(`channel.info`, params); const result: any = await sdk.get(`${restTypes[type]}.info`, params);
if (result.success) { if (result.success) {
const room = result[type]; const room = result[type];
room.rid = room._id; room.rid = room._id;

View File

@ -1,4 +1,4 @@
import { IRoom } from '../../definitions'; import { IRoom, SubscriptionType } from '../../definitions';
import { getSubscriptionByRoomId } from '../database/services/Subscription'; import { getSubscriptionByRoomId } from '../database/services/Subscription';
import RocketChat from '../rocketchat'; import RocketChat from '../rocketchat';
@ -10,7 +10,7 @@ const getRoomInfo = async (rid: string): Promise<Pick<IRoom, 'rid' | 'name' | 'f
rid, rid,
name: result.name, name: result.name,
fname: result.fname, fname: result.fname,
t: result.t t: result.t as SubscriptionType
}; };
} }

View File

@ -10,7 +10,7 @@ import updateMessages from './updateMessages';
const COUNT = 50; const COUNT = 50;
async function load({ rid: roomId, latest, t }: { rid: string; latest?: string; t: RoomTypes }) { async function load({ rid: roomId, latest, t }: { rid: string; latest?: Date; t: RoomTypes }) {
let params = { roomId, count: COUNT } as { roomId: string; count: number; latest?: string }; let params = { roomId, count: COUNT } as { roomId: string; count: number; latest?: string };
if (latest) { if (latest) {
params = { ...params, latest: new Date(latest).toISOString() }; params = { ...params, latest: new Date(latest).toISOString() };
@ -32,9 +32,9 @@ async function load({ rid: roomId, latest, t }: { rid: string; latest?: string;
export default function loadMessagesForRoom(args: { export default function loadMessagesForRoom(args: {
rid: string; rid: string;
t: RoomTypes; t: RoomTypes;
latest: string; latest?: Date;
loaderItem: TMessageModel; loaderItem?: TMessageModel;
}): Promise<Partial<IMessage>[]> { }): Promise<void> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const data: Partial<IMessage>[] = await load(args); const data: Partial<IMessage>[] = await load(args);
@ -52,9 +52,9 @@ export default function loadMessagesForRoom(args: {
data.push(loadMoreMessage); data.push(loadMoreMessage);
} }
await updateMessages({ rid: args.rid, update: data, loaderItem: args.loaderItem }); await updateMessages({ rid: args.rid, update: data, loaderItem: args.loaderItem });
return resolve(data); return resolve();
} }
return resolve([]); return resolve();
} catch (e) { } catch (e) {
log(e); log(e);
reject(e); reject(e);

View File

@ -16,7 +16,7 @@ const getLastUpdate = async (rid: string) => {
return null; return null;
}; };
async function load({ rid: roomId, lastOpen }: { rid: string; lastOpen: string }) { async function load({ rid: roomId, lastOpen }: { rid: string; lastOpen: Date }) {
let lastUpdate; let lastUpdate;
if (lastOpen) { if (lastOpen) {
lastUpdate = new Date(lastOpen).toISOString(); lastUpdate = new Date(lastOpen).toISOString();
@ -29,7 +29,7 @@ async function load({ rid: roomId, lastOpen }: { rid: string; lastOpen: string }
return result; return result;
} }
export default function loadMissedMessages(args: { rid: string; lastOpen: string }): Promise<void> { export default function loadMissedMessages(args: { rid: string; lastOpen: Date }): Promise<void> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const data = await load({ rid: args.rid, lastOpen: args.lastOpen }); const data = await load({ rid: args.rid, lastOpen: args.lastOpen });

View File

@ -7,19 +7,19 @@ import { getMessageById } from '../database/services/Message';
import { MESSAGE_TYPE_LOAD_NEXT_CHUNK } from '../../constants/messageTypeLoad'; import { MESSAGE_TYPE_LOAD_NEXT_CHUNK } from '../../constants/messageTypeLoad';
import { generateLoadMoreId } from '../utils'; import { generateLoadMoreId } from '../utils';
import updateMessages from './updateMessages'; import updateMessages from './updateMessages';
import { IMessage, TMessageModel } from '../../definitions'; import { TMessageModel } from '../../definitions';
import RocketChat from '../rocketchat'; import RocketChat from '../rocketchat';
const COUNT = 50; const COUNT = 50;
interface ILoadNextMessages { interface ILoadNextMessages {
rid: string; rid: string;
ts: string; ts: Date;
tmid: string; tmid?: string;
loaderItem: TMessageModel; loaderItem: TMessageModel;
} }
export default function loadNextMessages(args: ILoadNextMessages): Promise<IMessage | []> { export default function loadNextMessages(args: ILoadNextMessages): Promise<void> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const data = await RocketChat.methodCallWrapper('loadNextMessages', args.rid, args.ts, COUNT); const data = await RocketChat.methodCallWrapper('loadNextMessages', args.rid, args.ts, COUNT);
@ -39,9 +39,9 @@ export default function loadNextMessages(args: ILoadNextMessages): Promise<IMess
messages.push(loadMoreItem); messages.push(loadMoreItem);
} }
await updateMessages({ rid: args.rid, update: messages, loaderItem: args.loaderItem }); await updateMessages({ rid: args.rid, update: messages, loaderItem: args.loaderItem });
return resolve(messages); return resolve();
} }
return resolve([]); return resolve();
} catch (e) { } catch (e) {
log(e); log(e);
reject(e); reject(e);

View File

@ -1,76 +0,0 @@
import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import EJSON from 'ejson';
import database from '../database';
import log from '../../utils/log';
import { Encryption } from '../encryption';
import protectedFunction from './helpers/protectedFunction';
import buildMessage from './helpers/buildMessage';
async function load({ tmid }) {
try {
// RC 1.0
const result = await this.methodCallWrapper('getThreadMessages', { tmid });
if (!result) {
return [];
}
return EJSON.fromJSONValue(result);
} catch (error) {
console.log(error);
return [];
}
}
export default function loadThreadMessages({ tmid, rid }) {
return new Promise(async (resolve, reject) => {
try {
let data = await load.call(this, { tmid });
if (data && data.length) {
try {
data = data.filter(m => m.tmid).map(m => buildMessage(m));
data = await Encryption.decryptMessages(data);
const db = database.active;
const threadMessagesCollection = db.get('thread_messages');
const allThreadMessagesRecords = await threadMessagesCollection.query(Q.where('rid', tmid)).fetch();
let threadMessagesToCreate = data.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id));
let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => data.find(i2 => i1.id === i2._id));
threadMessagesToCreate = threadMessagesToCreate.map(threadMessage =>
threadMessagesCollection.prepareCreate(
protectedFunction(tm => {
tm._raw = sanitizedRaw({ id: threadMessage._id }, threadMessagesCollection.schema);
Object.assign(tm, threadMessage);
tm.subscription.id = rid;
tm.rid = threadMessage.tmid;
delete threadMessage.tmid;
})
)
);
threadMessagesToUpdate = threadMessagesToUpdate.map(threadMessage => {
const newThreadMessage = data.find(t => t._id === threadMessage.id);
return threadMessage.prepareUpdate(
protectedFunction(tm => {
Object.assign(tm, newThreadMessage);
tm.rid = threadMessage.tmid;
delete threadMessage.tmid;
})
);
});
await db.action(async () => {
await db.batch(...threadMessagesToCreate, ...threadMessagesToUpdate);
});
} catch (e) {
log(e);
}
return resolve(data);
} else {
return resolve([]);
}
} catch (e) {
reject(e);
}
});
}

View File

@ -0,0 +1,87 @@
import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import EJSON from 'ejson';
import database from '../database';
import log from '../../utils/log';
import { Encryption } from '../encryption';
import protectedFunction from './helpers/protectedFunction';
import buildMessage from './helpers/buildMessage';
import { TThreadMessageModel } from '../../definitions';
import sdk from '../rocketchat/services/sdk';
async function load({ tmid }: { tmid: string }) {
try {
// RC 1.0
const result = await sdk.methodCallWrapper('getThreadMessages', { tmid });
if (!result) {
return [];
}
return EJSON.fromJSONValue(result);
} catch (error) {
console.log(error);
return [];
}
}
export default function loadThreadMessages({ tmid, rid }: { tmid: string; rid: string }) {
return new Promise<void>(async (resolve, reject) => {
try {
let data = await load({ tmid });
if (data && data.length) {
try {
data = data.filter((m: TThreadMessageModel) => m.tmid).map((m: TThreadMessageModel) => buildMessage(m));
data = await Encryption.decryptMessages(data);
const db = database.active;
const threadMessagesCollection = db.get('thread_messages');
const allThreadMessagesRecords = await threadMessagesCollection.query(Q.where('rid', tmid)).fetch();
const filterThreadMessagesToCreate = data.filter(
(i1: TThreadMessageModel) => !allThreadMessagesRecords.find(i2 => i1._id === i2.id)
);
const filterThreadMessagesToUpdate = allThreadMessagesRecords.filter(i1 =>
data.find((i2: TThreadMessageModel) => i1.id === i2._id)
);
const threadMessagesToCreate = filterThreadMessagesToCreate.map((threadMessage: TThreadMessageModel) =>
threadMessagesCollection.prepareCreate(
protectedFunction((tm: TThreadMessageModel) => {
tm._raw = sanitizedRaw({ id: threadMessage._id }, threadMessagesCollection.schema);
Object.assign(tm, threadMessage);
if (tm.subscription) {
tm.subscription.id = rid;
}
if (threadMessage.tmid) {
tm.rid = threadMessage.tmid;
}
delete threadMessage.tmid;
})
)
);
const threadMessagesToUpdate = filterThreadMessagesToUpdate.map(threadMessage => {
const newThreadMessage = data.find((t: TThreadMessageModel) => t._id === threadMessage.id);
return threadMessage.prepareUpdate(
protectedFunction((tm: TThreadMessageModel) => {
Object.assign(tm, newThreadMessage);
if (threadMessage.tmid) {
tm.rid = threadMessage.tmid;
}
delete threadMessage.tmid;
})
);
});
await db.write(async () => {
await db.batch(...threadMessagesToCreate, ...threadMessagesToUpdate);
});
} catch (e) {
log(e);
}
return resolve(data);
}
return resolve();
} catch (e) {
reject(e);
}
});
}

View File

@ -1,15 +1,16 @@
import database from '../database'; import database from '../database';
import log from '../../utils/log'; import log from '../../utils/log';
import { TSubscriptionModel } from '../../definitions'; import { TSubscriptionModel } from '../../definitions';
import { IRocketChat } from '../../definitions/IRocketChat'; import sdk from '../rocketchat/services/sdk';
export default async function readMessages(this: IRocketChat, rid: string, ls: Date, updateLastOpen = false): Promise<void> { export default async function readMessages(rid: string, ls: Date, updateLastOpen = false): Promise<void> {
try { try {
const db = database.active; const db = database.active;
const subscription = await db.get('subscriptions').find(rid); const subscription = await db.get('subscriptions').find(rid);
// RC 0.61.0 // RC 0.61.0
await this.sdk.post('subscriptions.read', { rid }); // @ts-ignore
await sdk.post('subscriptions.read', { rid });
await db.write(async () => { await db.write(async () => {
try { try {

View File

@ -37,9 +37,9 @@ export async function cancelUpload(item: TUploadModel): Promise<void> {
export function sendFileMessage( export function sendFileMessage(
rid: string, rid: string,
fileInfo: IUpload, fileInfo: IUpload,
tmid: string, tmid: string | undefined,
server: string, server: string,
user: IUser user: Partial<Pick<IUser, 'id' | 'token'>>
): Promise<FetchBlobResponse | void> { ): Promise<FetchBlobResponse | void> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {

View File

@ -85,7 +85,13 @@ export async function resendMessage(message: TMessageModel, tmid?: string) {
} }
} }
export default async function (rid: string, msg: string, tmid: string, user: IUser, tshow?: boolean) { export default async function (
rid: string,
msg: string,
tmid: string | undefined,
user: Partial<Pick<IUser, 'id' | 'username' | 'name'>>,
tshow?: boolean
): Promise<void> {
try { try {
const db = database.active; const db = database.active;
const subsCollection = db.get('subscriptions'); const subsCollection = db.get('subscriptions');

View File

@ -105,7 +105,9 @@ export default async function updateMessages({
threadCollection.prepareCreate( threadCollection.prepareCreate(
protectedFunction((t: TThreadModel) => { protectedFunction((t: TThreadModel) => {
t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema); t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema);
if (t.subscription) t.subscription.id = sub.id; if (t.subscription) {
t.subscription.id = sub.id;
}
Object.assign(t, thread); Object.assign(t, thread);
}) })
) )

View File

@ -885,7 +885,7 @@ const RocketChat = {
} }
const autoTranslatePermission = reduxStore.getState().permissions['auto-translate']; const autoTranslatePermission = reduxStore.getState().permissions['auto-translate'];
const userRoles = reduxStore.getState().login?.user?.roles ?? []; const userRoles = reduxStore.getState().login?.user?.roles ?? [];
return autoTranslatePermission?.some(role => userRoles.includes(role)); return autoTranslatePermission?.some(role => userRoles.includes(role)) ?? false;
} catch (e) { } catch (e) {
log(e); log(e);
return false; return false;

View File

@ -247,7 +247,7 @@ export const convertTeamToChannel = ({ teamId, selected }: { teamId: string; sel
return sdk.post('teams.convertToChannel', params); return sdk.post('teams.convertToChannel', params);
}; };
export const joinRoom = (roomId: string, joinCode: string, type: 'c' | 'p'): any => { export const joinRoom = (roomId: string, joinCode: string | null, type: 'c' | 'p'): any => {
// TODO: join code // TODO: join code
// RC 0.48.0 // RC 0.48.0
if (type === 'p') { if (type === 'p') {

View File

@ -1,6 +1,6 @@
import { addSettings, clearSettings, updateSettings } from '../actions/settings'; import { addSettings, clearSettings, updateSettings } from '../actions/settings';
import { mockedStore } from './mockedStore'; import { mockedStore } from './mockedStore';
import { initialState } from './settings'; import { initialState, TSettingsState } from './settings';
describe('test settings reducer', () => { describe('test settings reducer', () => {
it('should return initial state', () => { it('should return initial state', () => {
@ -8,7 +8,11 @@ describe('test settings reducer', () => {
expect(state).toEqual(initialState); expect(state).toEqual(initialState);
}); });
const settings = { API_Use_REST_For_DDP_Calls: true, FileUpload_MaxFileSize: 600857600, Jitsi_URL_Room_Prefix: 'RocketChat' }; const settings: TSettingsState = {
API_Use_REST_For_DDP_Calls: true,
FileUpload_MaxFileSize: 600857600,
Jitsi_URL_Room_Prefix: 'RocketChat'
};
it('should return modified store after call addSettings action', () => { it('should return modified store after call addSettings action', () => {
mockedStore.dispatch(addSettings(settings)); mockedStore.dispatch(addSettings(settings));

View File

@ -1,13 +1,17 @@
import { IActionSettings } from '../actions/settings'; import { IActionSettings } from '../actions/settings';
import { SETTINGS } from '../actions/actionsTypes'; import { SETTINGS } from '../actions/actionsTypes';
import settings from '../constants/settings';
export type TSettings = string | number | boolean; export type TSupportedSettings = keyof typeof settings;
export type TSettingsValues = string | number | boolean | string[];
export type ISettings = Record<string, TSettings>; export type TSettingsState = {
[K in TSupportedSettings]?: TSettingsValues;
};
export const initialState: ISettings = {}; export const initialState: TSettingsState = {};
export default (state = initialState, action: IActionSettings): ISettings => { export default (state = initialState, action: IActionSettings): TSettingsState => {
switch (action.type) { switch (action.type) {
case SETTINGS.ADD: case SETTINGS.ADD:
return { return {

View File

@ -19,34 +19,37 @@ export type ChatsStackParamList = {
NewMessageStackNavigator: any; NewMessageStackNavigator: any;
NewMessageStack: undefined; NewMessageStack: undefined;
RoomsListView: undefined; RoomsListView: undefined;
RoomView?: { RoomView:
rid: string; | {
t: SubscriptionType; rid: string;
tmid?: string; t: SubscriptionType;
message?: string; tmid?: string;
name?: string; message?: object; // TODO: TMessageModel?
fname?: string; name?: string;
prid?: string; fname?: string;
room?: ISubscription; prid?: string;
jumpToMessageId?: string; room?: TSubscriptionModel | { rid: string; t: string; name?: string; fname?: string; prid?: string };
jumpToThreadId?: string; jumpToMessageId?: string;
roomUserId?: string; jumpToThreadId?: string;
}; roomUserId?: string | null;
usedCannedResponse?: string;
}
| undefined; // Navigates back to RoomView already on stack
RoomActionsView: { RoomActionsView: {
room: ISubscription; room: TSubscriptionModel;
member: any; member: any;
rid: string; rid: string;
t: SubscriptionType; t: SubscriptionType;
joined: boolean; joined: boolean;
}; };
SelectListView: { SelectListView: {
data: IRoom[]; data?: IRoom[];
title: string; title: string;
infoText: string; infoText?: string;
nextAction: (selected: string[]) => void; nextAction: (selected: string[]) => void;
showAlert: () => void; showAlert?: () => void;
isSearch: boolean; isSearch?: boolean;
onSearch: (text: string) => Partial<IRoom[]>; onSearch?: (text: string) => Promise<Partial<IRoom[]> | any>;
isRadio?: boolean; isRadio?: boolean;
}; };
RoomInfoView: { RoomInfoView: {
@ -240,7 +243,7 @@ export type InsideStackParamList = {
isShareExtension: boolean; isShareExtension: boolean;
serverInfo: IServer; serverInfo: IServer;
text: string; text: string;
room: ISubscription; room: TSubscriptionModel;
thread: any; // TODO: Change thread: any; // TODO: Change
}; };
ModalBlockView: { ModalBlockView: {

View File

@ -1,9 +1,9 @@
import { ChatsStackParamList } from '../stacks/types'; import { ChatsStackParamList } from '../stacks/types';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { IVisitor, SubscriptionType } from '../definitions/ISubscription'; import { ISubscription, IVisitor, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription';
export interface IGoRoomItem { interface IGoRoomItem {
search?: boolean; // comes from spotlight search?: boolean; // comes from spotlight
username?: string; username?: string;
t?: SubscriptionType; t?: SubscriptionType;
@ -13,12 +13,14 @@ export interface IGoRoomItem {
visitor?: IVisitor; visitor?: IVisitor;
} }
export type TGoRoomItem = IGoRoomItem | TSubscriptionModel | ISubscription;
const navigate = ({ const navigate = ({
item, item,
isMasterDetail, isMasterDetail,
...props ...props
}: { }: {
item: IGoRoomItem; item: TGoRoomItem;
isMasterDetail: boolean; isMasterDetail: boolean;
navigationMethod?: () => ChatsStackParamList; navigationMethod?: () => ChatsStackParamList;
}) => { }) => {
@ -45,13 +47,13 @@ export const goRoom = async ({
isMasterDetail = false, isMasterDetail = false,
...props ...props
}: { }: {
item: IGoRoomItem; item: TGoRoomItem;
isMasterDetail: boolean; isMasterDetail: boolean;
navigationMethod?: any; navigationMethod?: any;
jumpToMessageId?: string; jumpToMessageId?: string;
usedCannedResponse?: string; usedCannedResponse?: string;
}): Promise<void> => { }): Promise<void> => {
if (item.t === SubscriptionType.DIRECT && item?.search) { if (!('id' in item) && item.t === SubscriptionType.DIRECT && item?.search) {
// if user is using the search we need first to join/create room // if user is using the search we need first to join/create room
try { try {
const { username } = item; const { username } = item;

View File

@ -9,21 +9,18 @@ const canPostReadOnly = async ({ rid }: { rid: string }) => {
return permission[0]; return permission[0];
}; };
const isMuted = (room: ISubscription, user: { username: string }) => const isMuted = (room: Partial<ISubscription>, username: string) =>
room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username); room && room.muted && room.muted.find && !!room.muted.find(m => m === username);
export const isReadOnly = async ( export const isReadOnly = async (room: Partial<ISubscription>, username: string): Promise<boolean> => {
room: ISubscription,
user: { id?: string; username: string; token?: string }
): Promise<boolean> => {
if (room.archived) { if (room.archived) {
return true; return true;
} }
if (isMuted(room, user)) { if (isMuted(room, username)) {
return true; return true;
} }
if (room?.ro) { if (room?.ro) {
const allowPost = await canPostReadOnly(room); const allowPost = await canPostReadOnly({ rid: room.rid as string });
if (allowPost) { if (allowPost) {
return false; return false;
} }

View File

@ -3,9 +3,9 @@ import moment from 'moment';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import I18n from '../i18n'; import I18n from '../i18n';
import { IAttachment } from '../definitions/IAttachment'; import { IAttachment } from '../definitions/IAttachment';
import { ISubscription, SubscriptionType } from '../definitions/ISubscription'; import { SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription';
export const isBlocked = (room: ISubscription): boolean => { export const isBlocked = (room: TSubscriptionModel): boolean => {
if (room) { if (room) {
const { t, blocked, blocker } = room; const { t, blocked, blocker } = room;
if (t === SubscriptionType.DIRECT && (blocked || blocker)) { if (t === SubscriptionType.DIRECT && (blocked || blocker)) {
@ -62,4 +62,4 @@ export const getBadgeColor = ({
export const makeThreadName = (messageRecord: { id?: string; msg?: string; attachments?: IAttachment[] }): string | undefined => export const makeThreadName = (messageRecord: { id?: string; msg?: string; attachments?: IAttachment[] }): string | undefined =>
messageRecord.msg || messageRecord?.attachments?.[0]?.title; messageRecord.msg || messageRecord?.attachments?.[0]?.title;
export const isTeamRoom = ({ teamId, joined }: { teamId: string; joined: boolean }): boolean => !!teamId && joined; export const isTeamRoom = ({ teamId, joined }: { teamId?: string; joined?: boolean }): boolean => (!!teamId && joined) ?? false;

View File

@ -115,7 +115,7 @@ const CannedResponseDetail = ({ navigation, route }: ICannedResponseDetailProps)
t: room.t, t: room.t,
fname: name fname: name
}), }),
t: room.t, t: room.t as any,
roomUserId: RocketChat.getUidDirectMessage(room), roomUserId: RocketChat.getUidDirectMessage(room),
usedCannedResponse: item.text usedCannedResponse: item.text
}; };

View File

@ -118,7 +118,7 @@ const CannedResponsesListView = ({ navigation, route }: ICannedResponsesListView
t: room.t, t: room.t,
fname: name fname: name
}), }),
t: room.t, t: room.t as any,
roomUserId: RocketChat.getUidDirectMessage(room), roomUserId: RocketChat.getUidDirectMessage(room),
usedCannedResponse: item.text usedCannedResponse: item.text
}; };

View File

@ -37,10 +37,9 @@ interface IDiscussionDetails {
const DiscussionDetails = ({ item, date }: IDiscussionDetails): JSX.Element => { const DiscussionDetails = ({ item, date }: IDiscussionDetails): JSX.Element => {
const { theme } = useTheme(); const { theme } = useTheme();
let { dcount } = item; let count: string | number | undefined = item.dcount;
if (count && count >= 1000) {
if (dcount && dcount >= 1000) { count = '+999';
dcount = '+999';
} }
return ( return (
@ -49,7 +48,7 @@ const DiscussionDetails = ({ item, date }: IDiscussionDetails): JSX.Element => {
<View style={styles.detailContainer}> <View style={styles.detailContainer}>
<CustomIcon name={'discussions'} size={24} color={themes[theme!].auxiliaryText} /> <CustomIcon name={'discussions'} size={24} color={themes[theme!].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
{dcount} {count}
</Text> </Text>
</View> </View>

View File

@ -63,6 +63,7 @@ const Item = ({ item, onPress }: IItem): JSX.Element => {
if (item?.ts) { if (item?.ts) {
messageTime = moment(item.ts).format('LT'); messageTime = moment(item.ts).format('LT');
// @ts-ignore TODO: Unify IMessage
messageDate = formatDateThreads(item.ts); messageDate = formatDateThreads(item.ts);
} }

View File

@ -79,7 +79,7 @@ interface IParams {
rid: string; rid: string;
t: SubscriptionType; t: SubscriptionType;
tmid?: string; tmid?: string;
message?: string; message?: object;
name?: string; name?: string;
fname?: string; fname?: string;
prid?: string; prid?: string;
@ -202,7 +202,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
}, },
noDataMsg: I18n.t('No_files'), noDataMsg: I18n.t('No_files'),
testID: 'room-files-view', testID: 'room-files-view',
renderItem: (item: IMessageItem) => ( renderItem: (item: any) => (
<Message <Message
{...renderItemCommonProps(item)} {...renderItemCommonProps(item)}
item={{ item={{
@ -231,6 +231,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
}, },
noDataMsg: I18n.t('No_mentioned_messages'), noDataMsg: I18n.t('No_mentioned_messages'),
testID: 'mentioned-messages-view', testID: 'mentioned-messages-view',
// @ts-ignore TODO: unify IMessage
renderItem: (item: IMessageItem) => <Message {...renderItemCommonProps(item)} msg={item.msg} theme={theme} /> renderItem: (item: IMessageItem) => <Message {...renderItemCommonProps(item)} msg={item.msg} theme={theme} />
}, },
// Starred Messages Screen // Starred Messages Screen
@ -244,6 +245,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
noDataMsg: I18n.t('No_starred_messages'), noDataMsg: I18n.t('No_starred_messages'),
testID: 'starred-messages-view', testID: 'starred-messages-view',
renderItem: (item: IMessageItem) => ( renderItem: (item: IMessageItem) => (
// @ts-ignore TODO: unify IMessage
<Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} /> <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} />
), ),
action: (message: IMessageItem) => ({ action: (message: IMessageItem) => ({
@ -264,6 +266,7 @@ class MessagesView extends React.Component<IMessagesViewProps, any> {
noDataMsg: I18n.t('No_pinned_messages'), noDataMsg: I18n.t('No_pinned_messages'),
testID: 'pinned-messages-view', testID: 'pinned-messages-view',
renderItem: (item: IMessageItem) => ( renderItem: (item: IMessageItem) => (
// @ts-ignore TODO: unify IMessage
<Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} /> <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} />
), ),
action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }), action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }),

View File

@ -1,38 +1,94 @@
import { Q } from '@nozbe/watermelondb';
import { StackNavigationOptions } from '@react-navigation/stack';
import isEmpty from 'lodash/isEmpty';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Share, Switch, Text, View } from 'react-native'; import { Share, Switch, Text, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import isEmpty from 'lodash/isEmpty'; import { Observable, Subscription } from 'rxjs';
import { Q } from '@nozbe/watermelondb';
import { compareServerVersion } from '../../lib/utils';
import Touch from '../../utils/touch';
import { setLoading } from '../../actions/selectedUsers';
import { closeRoom, leaveRoom } from '../../actions/room'; import { closeRoom, leaveRoom } from '../../actions/room';
import sharedStyles from '../Styles'; import { setLoading } from '../../actions/selectedUsers';
import Avatar from '../../containers/Avatar';
import Status from '../../containers/Status';
import * as List from '../../containers/List';
import RocketChat from '../../lib/rocketchat';
import log, { events, logEvent } from '../../utils/log';
import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar';
import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import Avatar from '../../containers/Avatar';
import * as HeaderButton from '../../containers/HeaderButton'; import * as HeaderButton from '../../containers/HeaderButton';
import * as List from '../../containers/List';
import { MarkdownPreview } from '../../containers/markdown'; import { MarkdownPreview } from '../../containers/markdown';
import { showConfirmationAlert, showErrorAlert } from '../../utils/info'; import RoomTypeIcon from '../../containers/RoomTypeIcon';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import Status from '../../containers/Status';
import StatusBar from '../../containers/StatusBar';
import { IApplicationState, IBaseScreen, IRoom, ISubscription, IUser, TSubscriptionModel } from '../../definitions';
import { withDimensions } from '../../dimensions';
import I18n from '../../i18n';
import database from '../../lib/database';
import { E2E_ROOM_TYPES } from '../../lib/encryption/constants'; import { E2E_ROOM_TYPES } from '../../lib/encryption/constants';
import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import database from '../../lib/database'; import RocketChat from '../../lib/rocketchat';
import { withDimensions } from '../../dimensions'; import { compareServerVersion } from '../../lib/utils';
import { getUserSelector } from '../../selectors/login';
import { ChatsStackParamList } from '../../stacks/types';
import { withTheme } from '../../theme';
import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
import log, { events, logEvent } from '../../utils/log';
import Touch from '../../utils/touch';
import sharedStyles from '../Styles';
import styles from './styles'; import styles from './styles';
import { ERoomType } from '../../definitions/ERoomType';
class RoomActionsView extends React.Component { interface IRoomActionsViewProps extends IBaseScreen<ChatsStackParamList, 'RoomActionsView'> {
static navigationOptions = ({ navigation, isMasterDetail }) => { userId: string;
const options = { jitsiEnabled: boolean;
jitsiEnableTeams: boolean;
jitsiEnableChannels: boolean;
encryptionEnabled: boolean;
fontScale: number;
serverVersion: string | null;
addUserToJoinedRoomPermission?: string[];
addUserToAnyCRoomPermission?: string[];
addUserToAnyPRoomPermission?: string[];
createInviteLinksPermission?: string[];
editRoomPermission?: string[];
toggleRoomE2EEncryptionPermission?: string[];
viewBroadcastMemberListPermission?: string[];
transferLivechatGuestPermission?: string[];
createTeamPermission?: string[];
addTeamChannelPermission?: string[];
convertTeamPermission?: string[];
viewCannedResponsesPermission?: string[];
}
interface IRoomActionsViewState {
room: TSubscriptionModel;
membersCount: number;
member: Partial<IUser>;
joined: boolean;
canViewMembers: boolean;
canAutoTranslate: boolean;
canAddUser: boolean;
canInviteUser: boolean;
canForwardGuest: boolean;
canReturnQueue: boolean;
canEdit: boolean;
canToggleEncryption: boolean;
canCreateTeam: boolean;
canAddChannelToTeam: boolean;
canConvertTeam: boolean;
canViewCannedResponse: boolean;
}
class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomActionsViewState> {
private mounted: boolean;
private rid: string;
private t: string;
private joined: boolean;
private roomObservable?: Observable<TSubscriptionModel>;
private subscription?: Subscription;
static navigationOptions = ({
navigation,
isMasterDetail
}: Pick<IRoomActionsViewProps, 'navigation' | 'isMasterDetail'>): StackNavigationOptions => {
const options: StackNavigationOptions = {
title: I18n.t('Actions') title: I18n.t('Actions')
}; };
if (isMasterDetail) { if (isMasterDetail) {
@ -41,34 +97,7 @@ class RoomActionsView extends React.Component {
return options; return options;
}; };
static propTypes = { constructor(props: IRoomActionsViewProps) {
navigation: PropTypes.object,
route: PropTypes.object,
leaveRoom: PropTypes.func,
jitsiEnabled: PropTypes.bool,
jitsiEnableTeams: PropTypes.bool,
jitsiEnableChannels: PropTypes.bool,
encryptionEnabled: PropTypes.bool,
setLoadingInvite: PropTypes.func,
closeRoom: PropTypes.func,
theme: PropTypes.string,
fontScale: PropTypes.number,
serverVersion: PropTypes.string,
addUserToJoinedRoomPermission: PropTypes.array,
addUserToAnyCRoomPermission: PropTypes.array,
addUserToAnyPRoomPermission: PropTypes.array,
createInviteLinksPermission: PropTypes.array,
editRoomPermission: PropTypes.array,
toggleRoomE2EEncryptionPermission: PropTypes.array,
viewBroadcastMemberListPermission: PropTypes.array,
transferLivechatGuestPermission: PropTypes.array,
createTeamPermission: PropTypes.array,
addTeamChannelPermission: PropTypes.array,
convertTeamPermission: PropTypes.array,
viewCannedResponsesPermission: PropTypes.array
};
constructor(props) {
super(props); super(props);
this.mounted = false; this.mounted = false;
const room = props.route.params?.room; const room = props.route.params?.room;
@ -77,7 +106,7 @@ class RoomActionsView extends React.Component {
this.t = props.route.params?.t; this.t = props.route.params?.t;
this.joined = props.route.params?.joined; this.joined = props.route.params?.joined;
this.state = { this.state = {
room: room || { rid: this.rid, t: this.t }, room: room || ({ rid: this.rid, t: this.t } as any),
membersCount: 0, membersCount: 0,
member: member || {}, member: member || {},
joined: !!room, joined: !!room,
@ -100,6 +129,7 @@ class RoomActionsView extends React.Component {
if (this.mounted) { if (this.mounted) {
this.setState({ room: changes }); this.setState({ room: changes });
} else { } else {
// @ts-ignore
this.state.room = changes; this.state.room = changes;
} }
}); });
@ -123,7 +153,7 @@ class RoomActionsView extends React.Component {
if (room && room.t !== 'd' && this.canViewMembers()) { if (room && room.t !== 'd' && this.canViewMembers()) {
try { try {
const counters = await RocketChat.getRoomCounters(room.rid, room.t); const counters = await RocketChat.getRoomCounters(room.rid, room.t as any);
if (counters.success) { if (counters.success) {
this.setState({ membersCount: counters.members, joined: counters.joined }); this.setState({ membersCount: counters.members, joined: counters.joined });
} }
@ -177,9 +207,15 @@ class RoomActionsView extends React.Component {
return room.t === 'l' && room.status === 'queued' && !this.joined; return room.t === 'l' && room.status === 'queued' && !this.joined;
} }
onPressTouchable = item => { // TODO: assert params required for navigation
onPressTouchable = (item: { route?: keyof ChatsStackParamList; params?: object; event?: Function }) => {
const { route, event, params } = item; const { route, event, params } = item;
if (route) { if (route) {
/**
* TODO: params can vary too much and ts is going to be happy
* Instead of playing with this, we should think on a better `logEvent` function
*/
// @ts-ignore
logEvent(events[`RA_GO_${route.replace('View', '').toUpperCase()}${params.name ? params.name.toUpperCase() : ''}`]); logEvent(events[`RA_GO_${route.replace('View', '').toUpperCase()}${params.name ? params.name.toUpperCase() : ''}`]);
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate(route, params); navigation.navigate(route, params);
@ -349,7 +385,7 @@ class RoomActionsView extends React.Component {
onPress: async () => { onPress: async () => {
try { try {
await RocketChat.returnLivechat(rid); await RocketChat.returnLivechat(rid);
} catch (e) { } catch (e: any) {
showErrorAlert(e.reason, I18n.t('Oops')); showErrorAlert(e.reason, I18n.t('Oops'));
} }
} }
@ -364,7 +400,7 @@ class RoomActionsView extends React.Component {
const roomUserId = RocketChat.getUidDirectMessage(room); const roomUserId = RocketChat.getUidDirectMessage(room);
const result = await RocketChat.getUserInfo(roomUserId); const result = await RocketChat.getUserInfo(roomUserId);
if (result.success) { if (result.success) {
this.setState({ member: result.user }); this.setState({ member: result.user as any });
} }
} }
} catch (e) { } catch (e) {
@ -394,7 +430,7 @@ class RoomActionsView extends React.Component {
const { rid, blocker } = room; const { rid, blocker } = room;
const { member } = this.state; const { member } = this.state;
try { try {
await RocketChat.toggleBlockUser(rid, member._id, !blocker); await RocketChat.toggleBlockUser(rid, member._id as string, !blocker);
} catch (e) { } catch (e) {
logEvent(events.RA_TOGGLE_BLOCK_USER_F); logEvent(events.RA_TOGGLE_BLOCK_USER_F);
log(e); log(e);
@ -411,9 +447,9 @@ class RoomActionsView extends React.Component {
const encrypted = !room.encrypted; const encrypted = !room.encrypted;
try { try {
// Instantly feedback to the user // Instantly feedback to the user
await db.action(async () => { await db.write(async () => {
await room.update( await room.update(
protectedFunction(r => { protectedFunction((r: TSubscriptionModel) => {
r.encrypted = encrypted; r.encrypted = encrypted;
}) })
); );
@ -431,9 +467,9 @@ class RoomActionsView extends React.Component {
} }
// If something goes wrong we go back to the previous value // If something goes wrong we go back to the previous value
await db.action(async () => { await db.write(async () => {
await room.update( await room.update(
protectedFunction(r => { protectedFunction((r: TSubscriptionModel) => {
r.encrypted = room.encrypted; r.encrypted = room.encrypted;
}) })
); );
@ -463,28 +499,31 @@ class RoomActionsView extends React.Component {
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }), message: I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }),
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
onPress: () => dispatch(leaveRoom('channel', room)) onPress: () => dispatch(leaveRoom(ERoomType.c, room))
}); });
}; };
convertTeamToChannel = async () => { convertTeamToChannel = async () => {
const { room } = this.state; const { room } = this.state;
const { navigation } = this.props; const { navigation, userId } = this.props;
try { try {
const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: room.u._id }); if (!room.teamId) {
return;
}
const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId });
if (result.rooms?.length) { if (result.rooms?.length) {
const teamChannels = result.rooms.map(r => ({ const teamChannels = result.rooms.map((r: any) => ({
rid: r._id, rid: r._id,
name: r.name, name: r.name,
teamId: r.teamId teamId: r.teamId
})); }));
navigation.navigate('SelectListView', { navigation.navigate('SelectListView', {
title: 'Converting_Team_To_Channel', title: 'Converting_Team_To_Channel',
data: teamChannels, data: teamChannels as any,
infoText: 'Select_Team_Channels_To_Delete', infoText: 'Select_Team_Channels_To_Delete',
nextAction: data => this.convertTeamToChannelConfirmation(data) nextAction: (data: string[]) => this.convertTeamToChannelConfirmation(data)
}); });
} else { } else {
this.convertTeamToChannelConfirmation(); this.convertTeamToChannelConfirmation();
@ -494,12 +533,15 @@ class RoomActionsView extends React.Component {
} }
}; };
handleConvertTeamToChannel = async selected => { handleConvertTeamToChannel = async (selected: string[]) => {
logEvent(events.RA_CONVERT_TEAM_TO_CHANNEL); logEvent(events.RA_CONVERT_TEAM_TO_CHANNEL);
try { try {
const { room } = this.state; const { room } = this.state;
const { navigation } = this.props; const { navigation } = this.props;
if (!room.teamId) {
return;
}
const result = await RocketChat.convertTeamToChannel({ teamId: room.teamId, selected }); const result = await RocketChat.convertTeamToChannel({ teamId: room.teamId, selected });
if (result.success) { if (result.success) {
@ -511,7 +553,7 @@ class RoomActionsView extends React.Component {
} }
}; };
convertTeamToChannelConfirmation = (selected = []) => { convertTeamToChannelConfirmation = (selected: string[] = []) => {
showConfirmationAlert({ showConfirmationAlert({
title: I18n.t('Confirmation'), title: I18n.t('Confirmation'),
message: I18n.t('You_are_converting_the_team'), message: I18n.t('You_are_converting_the_team'),
@ -522,13 +564,16 @@ class RoomActionsView extends React.Component {
leaveTeam = async () => { leaveTeam = async () => {
const { room } = this.state; const { room } = this.state;
const { navigation, dispatch } = this.props; const { navigation, dispatch, userId } = this.props;
try { try {
const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: room.u._id }); if (!room.teamId) {
return;
}
const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId });
if (result.rooms?.length) { if (result.rooms?.length) {
const teamChannels = result.rooms.map(r => ({ const teamChannels = result.rooms.map((r: any) => ({
rid: r._id, rid: r._id,
name: r.name, name: r.name,
teamId: r.teamId, teamId: r.teamId,
@ -538,21 +583,21 @@ class RoomActionsView extends React.Component {
title: 'Leave_Team', title: 'Leave_Team',
data: teamChannels, data: teamChannels,
infoText: 'Select_Team_Channels', infoText: 'Select_Team_Channels',
nextAction: data => dispatch(leaveRoom('team', room, data)), nextAction: data => dispatch(leaveRoom(ERoomType.t, room, data)),
showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_leave')) showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_leave'))
}); });
} else { } else {
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }), message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }),
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
onPress: () => dispatch(leaveRoom('team', room)) onPress: () => dispatch(leaveRoom(ERoomType.t, room))
}); });
} }
} catch (e) { } catch (e) {
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }), message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }),
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }),
onPress: () => dispatch(leaveRoom('team', room)) onPress: () => dispatch(leaveRoom(ERoomType.t, room))
}); });
} }
}; };
@ -562,7 +607,7 @@ class RoomActionsView extends React.Component {
try { try {
const { room } = this.state; const { room } = this.state;
const { navigation } = this.props; const { navigation } = this.props;
const result = await RocketChat.convertChannelToTeam({ rid: room.rid, name: room.name, type: room.t }); const result = await RocketChat.convertChannelToTeam({ rid: room.rid, name: room.name, type: room.t as any });
if (result.success) { if (result.success) {
navigation.navigate('RoomView'); navigation.navigate('RoomView');
@ -582,7 +627,7 @@ class RoomActionsView extends React.Component {
}); });
}; };
handleMoveToTeam = async selected => { handleMoveToTeam = async (selected: string[]) => {
logEvent(events.RA_MOVE_TO_TEAM); logEvent(events.RA_MOVE_TO_TEAM);
try { try {
const { room } = this.state; const { room } = this.state;
@ -603,14 +648,14 @@ class RoomActionsView extends React.Component {
const { navigation } = this.props; const { navigation } = this.props;
const db = database.active; const db = database.active;
const subCollection = db.get('subscriptions'); const subCollection = db.get('subscriptions');
const teamRooms = await subCollection.query(Q.where('team_main', true)); const teamRooms = await subCollection.query(Q.where('team_main', true)).fetch();
if (teamRooms.length) { if (teamRooms.length) {
const data = teamRooms.map(team => ({ const data = teamRooms.map(team => ({
rid: team.teamId, rid: team.teamId,
t: team.t, t: team.t,
name: team.name name: team.name
})); })) as IRoom[]; // TODO: review this usage later
navigation.navigate('SelectListView', { navigation.navigate('SelectListView', {
title: 'Move_to_Team', title: 'Move_to_Team',
infoText: 'Move_Channel_Paragraph', infoText: 'Move_Channel_Paragraph',
@ -637,7 +682,7 @@ class RoomActionsView extends React.Component {
} }
}; };
searchTeam = async onChangeText => { searchTeam = async (onChangeText: string) => {
logEvent(events.RA_SEARCH_TEAM); logEvent(events.RA_SEARCH_TEAM);
try { try {
const { addTeamChannelPermission, createTeamPermission } = this.props; const { addTeamChannelPermission, createTeamPermission } = this.props;
@ -650,9 +695,10 @@ class RoomActionsView extends React.Component {
Q.where('name', Q.like(`%${onChangeText}%`)), Q.where('name', Q.like(`%${onChangeText}%`)),
Q.experimentalTake(QUERY_SIZE), Q.experimentalTake(QUERY_SIZE),
Q.experimentalSortBy('room_updated_at', Q.desc) Q.experimentalSortBy('room_updated_at', Q.desc)
); )
.fetch();
const asyncFilter = async teamArray => { const asyncFilter = async (teamArray: TSubscriptionModel[]) => {
const results = await Promise.all( const results = await Promise.all(
teamArray.map(async team => { teamArray.map(async team => {
const permissions = await RocketChat.hasPermission([addTeamChannelPermission, createTeamPermission], team.rid); const permissions = await RocketChat.hasPermission([addTeamChannelPermission, createTeamPermission], team.rid);
@ -698,7 +744,6 @@ class RoomActionsView extends React.Component {
} }
style={{ backgroundColor: themes[theme].backgroundColor }} style={{ backgroundColor: themes[theme].backgroundColor }}
accessibilityLabel={I18n.t('Room_Info')} accessibilityLabel={I18n.t('Room_Info')}
accessibilityTraits='button'
enabled={!isGroupChat} enabled={!isGroupChat}
testID='room-actions-info' testID='room-actions-info'
theme={theme}> theme={theme}>
@ -708,7 +753,7 @@ class RoomActionsView extends React.Component {
<View style={[sharedStyles.status, { backgroundColor: themes[theme].backgroundColor }]}> <View style={[sharedStyles.status, { backgroundColor: themes[theme].backgroundColor }]}>
<Status size={16} id={member._id} /> <Status size={16} id={member._id} />
</View> </View>
) : null} ) : undefined}
</Avatar> </Avatar>
<View style={styles.roomTitleContainer}> <View style={styles.roomTitleContainer}>
{room.t === 'd' ? ( {room.t === 'd' ? (
@ -782,7 +827,7 @@ class RoomActionsView extends React.Component {
// If this room type can be encrypted // If this room type can be encrypted
// If e2e is enabled // If e2e is enabled
if (E2E_ROOM_TYPES[room?.t] && encryptionEnabled) { if (E2E_ROOM_TYPES[room.t] && encryptionEnabled) {
return ( return (
<List.Section> <List.Section>
<List.Separator /> <List.Separator />
@ -853,7 +898,7 @@ class RoomActionsView extends React.Component {
return null; return null;
}; };
teamChannelActions = (t, room) => { teamChannelActions = (t: string, room: ISubscription) => {
const { canEdit, canCreateTeam, canAddChannelToTeam } = this.state; const { canEdit, canCreateTeam, canAddChannelToTeam } = this.state;
const canConvertToTeam = canEdit && canCreateTeam && !room.teamMain; const canConvertToTeam = canEdit && canCreateTeam && !room.teamMain;
const canMoveToTeam = canEdit && canAddChannelToTeam && !room.teamId; const canMoveToTeam = canEdit && canAddChannelToTeam && !room.teamId;
@ -897,7 +942,7 @@ class RoomActionsView extends React.Component {
); );
}; };
teamToChannelActions = (t, room) => { teamToChannelActions = (t: string, room: ISubscription) => {
const { canEdit, canConvertTeam } = this.state; const { canEdit, canConvertTeam } = this.state;
const canConvertTeamToChannel = canEdit && canConvertTeam && !!room?.teamMain; const canConvertTeamToChannel = canEdit && canConvertTeam && !!room?.teamMain;
@ -952,7 +997,7 @@ class RoomActionsView extends React.Component {
<> <>
<List.Item <List.Item
title='Members' title='Members'
subtitle={membersCount > 0 ? `${membersCount} ${I18n.t('members')}` : null} subtitle={membersCount > 0 ? `${membersCount} ${I18n.t('members')}` : undefined}
onPress={() => this.onPressTouchable({ route: 'RoomMembersView', params: { rid, room } })} onPress={() => this.onPressTouchable({ route: 'RoomMembersView', params: { rid, room } })}
testID='room-actions-members' testID='room-actions-members'
left={() => <List.Icon name='team' />} left={() => <List.Icon name='team' />}
@ -1221,10 +1266,11 @@ class RoomActionsView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: IApplicationState) => ({
jitsiEnabled: state.settings.Jitsi_Enabled || false, userId: getUserSelector(state).id,
jitsiEnableTeams: state.settings.Jitsi_Enable_Teams || false, jitsiEnabled: (state.settings.Jitsi_Enabled || false) as boolean,
jitsiEnableChannels: state.settings.Jitsi_Enable_Channels || false, jitsiEnableTeams: (state.settings.Jitsi_Enable_Teams || false) as boolean,
jitsiEnableChannels: (state.settings.Jitsi_Enable_Channels || false) as boolean,
encryptionEnabled: state.encryption.enabled, encryptionEnabled: state.encryption.enabled,
serverVersion: state.server.version, serverVersion: state.server.version,
isMasterDetail: state.app.isMasterDetail, isMasterDetail: state.app.isMasterDetail,

View File

@ -16,7 +16,7 @@ import StatusBar from '../../containers/StatusBar';
import RCTextInput from '../../containers/TextInput'; import RCTextInput from '../../containers/TextInput';
import { LISTENER } from '../../containers/Toast'; import { LISTENER } from '../../containers/Toast';
import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { MultiSelect } from '../../containers/UIKit/MultiSelect';
import { IApplicationState, IBaseScreen, ISubscription, TSubscriptionModel } from '../../definitions'; import { IApplicationState, IBaseScreen, ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions';
import { ERoomType } from '../../definitions/ERoomType'; import { ERoomType } from '../../definitions/ERoomType';
import I18n from '../../i18n'; import I18n from '../../i18n';
import database from '../../lib/database'; import database from '../../lib/database';
@ -372,7 +372,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
title: 'Delete_Team', title: 'Delete_Team',
data: teamChannelOwner, data: teamChannelOwner,
infoText: 'Select_channels_to_delete', infoText: 'Select_channels_to_delete',
nextAction: (selected: Record<string, string>) => { nextAction: (selected: string[]) => {
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }), message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }),
confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }),
@ -437,7 +437,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
onPress: async () => { onPress: async () => {
try { try {
logEvent(events.RI_EDIT_TOGGLE_ARCHIVE); logEvent(events.RI_EDIT_TOGGLE_ARCHIVE);
await RocketChat.toggleArchiveRoom(rid, t, !archived); await RocketChat.toggleArchiveRoom(rid, t as SubscriptionType, !archived);
} catch (e) { } catch (e) {
logEvent(events.RI_EDIT_TOGGLE_ARCHIVE_F); logEvent(events.RI_EDIT_TOGGLE_ARCHIVE_F);
log(e); log(e);

View File

@ -24,7 +24,7 @@ import { getUserSelector } from '../../selectors/login';
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types'; import { ModalStackParamList } from '../../stacks/MasterDetailStack/types';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import { goRoom, IGoRoomItem } from '../../utils/goRoom'; import { goRoom, TGoRoomItem } from '../../utils/goRoom';
import { showConfirmationAlert, showErrorAlert } from '../../utils/info'; import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
import log from '../../utils/log'; import log from '../../utils/log';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
@ -469,7 +469,7 @@ class RoomMembersView extends React.Component<IRoomMembersViewProps, IRoomMember
} }
}; };
goRoom = (item: IGoRoomItem) => { goRoom = (item: TGoRoomItem) => {
const { navigation, isMasterDetail } = this.props; const { navigation, isMasterDetail } = this.props;
if (isMasterDetail) { if (isMasterDetail) {
// @ts-ignore // @ts-ignore

View File

@ -1,6 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import PropTypes from 'prop-types';
import { BorderlessButton, ScrollView } from 'react-native-gesture-handler'; import { BorderlessButton, ScrollView } from 'react-native-gesture-handler';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
@ -8,10 +7,19 @@ import Markdown, { MarkdownPreview } from '../../containers/markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
import { useTheme } from '../../theme';
interface IBannerProps {
text?: string;
title?: string;
bannerClosed?: boolean;
closeBanner: () => void;
}
const Banner = React.memo( const Banner = React.memo(
({ text, title, theme, bannerClosed, closeBanner }) => { ({ text, title, bannerClosed, closeBanner }: IBannerProps) => {
const [showModal, openModal] = useState(false); const [showModal, openModal] = useState(false);
const { theme } = useTheme();
const toggleModal = () => openModal(prevState => !prevState); const toggleModal = () => openModal(prevState => !prevState);
@ -47,16 +55,7 @@ const Banner = React.memo(
return null; return null;
}, },
(prevProps, nextProps) => (prevProps, nextProps) => prevProps.text === nextProps.text && prevProps.bannerClosed === nextProps.bannerClosed
prevProps.text === nextProps.text && prevProps.theme === nextProps.theme && prevProps.bannerClosed === nextProps.bannerClosed
); );
Banner.propTypes = {
text: PropTypes.string,
title: PropTypes.string,
theme: PropTypes.string,
bannerClosed: PropTypes.bool,
closeBanner: PropTypes.func
};
export default Banner; export default Banner;

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { ImageBackground, StyleSheet } from 'react-native'; import { ImageBackground, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { useTheme } from '../../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
image: { image: {
@ -10,17 +11,12 @@ const styles = StyleSheet.create({
} }
}); });
const EmptyRoom = React.memo(({ length, mounted, theme, rid }) => { const EmptyRoom = React.memo(({ length, mounted, rid }: { length: number; mounted: boolean; rid: string }) => {
const { theme } = useTheme();
if ((length === 0 && mounted) || !rid) { if ((length === 0 && mounted) || !rid) {
return <ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />; return <ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />;
} }
return null; return null;
}); });
EmptyRoom.propTypes = {
length: PropTypes.number.isRequired,
mounted: PropTypes.bool,
theme: PropTypes.string,
rid: PropTypes.string
};
export default EmptyRoom; export default EmptyRoom;

View File

@ -1,5 +1,4 @@
import React, { forwardRef, useImperativeHandle, useState } from 'react'; import React, { forwardRef, useImperativeHandle, useState } from 'react';
import PropTypes from 'prop-types';
import { InteractionManager, StyleSheet, Text, View } from 'react-native'; import { InteractionManager, StyleSheet, Text, View } from 'react-native';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -10,6 +9,7 @@ import TextInput from '../../containers/TextInput';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IApplicationState } from '../../definitions';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -41,8 +41,16 @@ const styles = StyleSheet.create({
} }
}); });
export interface IJoinCodeProps {
rid: string;
t: string;
onJoin: Function;
isMasterDetail: boolean;
theme: string;
}
const JoinCode = React.memo( const JoinCode = React.memo(
forwardRef(({ rid, t, onJoin, isMasterDetail, theme }, ref) => { forwardRef(({ rid, t, onJoin, isMasterDetail, theme }: IJoinCodeProps, ref) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [code, setCode] = useState(''); const [code, setCode] = useState('');
@ -53,7 +61,7 @@ const JoinCode = React.memo(
const joinRoom = async () => { const joinRoom = async () => {
try { try {
await RocketChat.joinRoom(rid, code, t); await RocketChat.joinRoom(rid, code, t as any);
onJoin(); onJoin();
hide(); hide();
} catch (e) { } catch (e) {
@ -64,7 +72,8 @@ const JoinCode = React.memo(
useImperativeHandle(ref, () => ({ show })); useImperativeHandle(ref, () => ({ show }));
return ( return (
<Modal transparent avoidKeyboard useNativeDriver isVisible={visible} hideModalContentWhileAnimating> // @ts-ignore TODO: `transparent` seems to exist, but types are incorrect on the lib
<Modal transparent={true} avoidKeyboard useNativeDriver isVisible={visible} hideModalContentWhileAnimating>
<View style={styles.container} testID='join-code'> <View style={styles.container} testID='join-code'>
<View <View
style={[ style={[
@ -76,14 +85,15 @@ const JoinCode = React.memo(
<TextInput <TextInput
value={code} value={code}
theme={theme} theme={theme}
inputRef={e => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())} // TODO: find a way to type this ref
inputRef={(e: any) => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
returnKeyType='send' returnKeyType='send'
autoCapitalize='none' autoCapitalize='none'
onChangeText={setCode} onChangeText={setCode}
onSubmitEditing={joinRoom} onSubmitEditing={joinRoom}
placeholder={I18n.t('Join_Code')} placeholder={I18n.t('Join_Code')}
secureTextEntry secureTextEntry
error={error && { error: 'error-code-invalid', reason: I18n.t('Code_or_password_invalid') }} error={error ? { error: 'error-code-invalid', reason: I18n.t('Code_or_password_invalid') } : undefined}
testID='join-code-input' testID='join-code-input'
/> />
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
@ -111,15 +121,8 @@ const JoinCode = React.memo(
); );
}) })
); );
JoinCode.propTypes = {
rid: PropTypes.string,
t: PropTypes.string,
onJoin: PropTypes.func,
isMasterDetail: PropTypes.bool,
theme: PropTypes.string
};
const mapStateToProps = state => ({ const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps, null, null, { forwardRef: true })(JoinCode); export default connect(mapStateToProps, null, null, { forwardRef: true })(JoinCode);

View File

@ -1,56 +0,0 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { StyleSheet } from 'react-native';
import { HeaderBackButton } from '@react-navigation/stack';
import { themes } from '../../constants/colors';
import Avatar from '../../containers/Avatar';
const styles = StyleSheet.create({
avatar: {
borderRadius: 10,
marginHorizontal: 16
}
});
const LeftButtons = React.memo(
({ tmid, unreadsCount, navigation, baseUrl, userId, token, title, t, theme, goRoomActionsView, isMasterDetail }) => {
if (!isMasterDetail || tmid) {
const onPress = useCallback(() => navigation.goBack());
const label = unreadsCount > 99 ? '+99' : unreadsCount || ' ';
const labelLength = label.length ? label.length : 1;
const marginLeft = -2 * labelLength;
const fontSize = labelLength > 1 ? 14 : 17;
return (
<HeaderBackButton
label={label}
onPress={onPress}
tintColor={themes[theme].headerTintColor}
labelStyle={{ fontSize, marginLeft }}
/>
);
}
const onPress = useCallback(() => goRoomActionsView(), []);
if (baseUrl && userId && token) {
return <Avatar text={title} size={30} type={t} style={styles.avatar} onPress={onPress} />;
}
return null;
}
);
LeftButtons.propTypes = {
tmid: PropTypes.string,
unreadsCount: PropTypes.number,
navigation: PropTypes.object,
baseUrl: PropTypes.string,
userId: PropTypes.string,
token: PropTypes.string,
title: PropTypes.string,
t: PropTypes.string,
theme: PropTypes.string,
goRoomActionsView: PropTypes.func,
isMasterDetail: PropTypes.bool
};
export default LeftButtons;

View File

@ -0,0 +1,72 @@
import React, { useCallback } from 'react';
import { StyleSheet } from 'react-native';
import { HeaderBackButton, StackNavigationProp } from '@react-navigation/stack';
import { themes } from '../../constants/colors';
import Avatar from '../../containers/Avatar';
import { ChatsStackParamList } from '../../stacks/types';
const styles = StyleSheet.create({
avatar: {
borderRadius: 10,
marginHorizontal: 16
}
});
interface ILeftButtonsProps {
tmid?: string;
unreadsCount: number | null;
navigation: StackNavigationProp<ChatsStackParamList, 'RoomView'>;
baseUrl: string;
userId?: string;
token?: string;
title?: string;
t: string;
theme: string;
goRoomActionsView: Function;
isMasterDetail: boolean;
}
const LeftButtons = ({
tmid,
unreadsCount,
navigation,
baseUrl,
userId,
token,
title,
t,
theme,
goRoomActionsView,
isMasterDetail
}: ILeftButtonsProps): React.ReactElement | null => {
if (!isMasterDetail || tmid) {
const onPress = () => navigation.goBack();
let label = ' ';
let labelLength = 1;
let marginLeft = 0;
let fontSize = 0;
if (unreadsCount) {
label = unreadsCount > 99 ? '+99' : unreadsCount.toString() || ' ';
labelLength = label.length ? label.length : 1;
marginLeft = -2 * labelLength;
fontSize = labelLength > 1 ? 14 : 17;
}
return (
<HeaderBackButton
label={label}
onPress={onPress}
tintColor={themes[theme].headerTintColor}
labelStyle={{ fontSize, marginLeft }}
/>
);
}
const onPress = useCallback(() => goRoomActionsView(), []);
if (baseUrl && userId && token) {
return <Avatar text={title} size={30} type={t} style={styles.avatar} onPress={onPress} />;
}
return null;
};
export default LeftButtons;

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { FlatList, StyleSheet } from 'react-native'; import { FlatList, FlatListProps, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated'; import Animated from 'react-native-reanimated';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -17,11 +17,15 @@ const styles = StyleSheet.create({
} }
}); });
const List = ({ listRef, ...props }) => ( export interface IListProps extends FlatListProps<any> {
listRef: any;
}
const List = ({ listRef, ...props }: IListProps) => (
<AnimatedFlatList <AnimatedFlatList
testID='room-view-messages' testID='room-view-messages'
ref={listRef} ref={listRef}
keyExtractor={item => item.id} keyExtractor={(item: any) => item.id}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
style={styles.list} style={styles.list}
inverted inverted

View File

@ -1,6 +1,5 @@
import React, { useCallback, useState } from 'react'; import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import Animated, { call, cond, greaterOrEq, useCode } from 'react-native-reanimated'; import Animated, { call, cond, greaterOrEq, useCode } from 'react-native-reanimated';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
@ -30,11 +29,19 @@ const styles = StyleSheet.create({
} }
}); });
const NavBottomFAB = ({ y, onPress, isThread }) => { const NavBottomFAB = ({
y,
onPress,
isThread
}: {
y: Animated.Value<number>;
onPress: Function;
isThread: boolean;
}): React.ReactElement | null => {
const { theme } = useTheme(); const { theme } = useTheme();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const handleOnPress = useCallback(() => onPress()); const handleOnPress = () => onPress();
const toggle = v => setShow(v); const toggle = (v: boolean) => setShow(v);
useCode( useCode(
() => () =>
@ -65,10 +72,4 @@ const NavBottomFAB = ({ y, onPress, isThread }) => {
); );
}; };
NavBottomFAB.propTypes = {
y: Animated.Value,
onPress: PropTypes.func,
isThread: PropTypes.bool
};
export default NavBottomFAB; export default NavBottomFAB;

View File

@ -1,26 +1,27 @@
import React from 'react';
import { RefreshControl } from 'react-native';
import PropTypes from 'prop-types';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import moment from 'moment';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { Value, event } from 'react-native-reanimated'; import moment from 'moment';
import React from 'react';
import { FlatListProps, RefreshControl, ViewToken } from 'react-native';
import { event, Value } from 'react-native-reanimated';
import { Observable, Subscription } from 'rxjs';
import { themes } from '../../../constants/colors';
import ActivityIndicator from '../../../containers/ActivityIndicator';
import { TAnyMessageModel, TMessageModel, TThreadMessageModel, TThreadModel } from '../../../definitions';
import database from '../../../lib/database'; import database from '../../../lib/database';
import RocketChat from '../../../lib/rocketchat'; import RocketChat from '../../../lib/rocketchat';
import { compareServerVersion } from '../../../lib/utils';
import debounce from '../../../utils/debounce';
import { animateNextTransition } from '../../../utils/layoutAnimation';
import log from '../../../utils/log'; import log from '../../../utils/log';
import EmptyRoom from '../EmptyRoom'; import EmptyRoom from '../EmptyRoom';
import { animateNextTransition } from '../../../utils/layoutAnimation'; import List, { IListProps } from './List';
import ActivityIndicator from '../../../containers/ActivityIndicator';
import { themes } from '../../../constants/colors';
import debounce from '../../../utils/debounce';
import { compareServerVersion } from '../../../lib/utils';
import List from './List';
import NavBottomFAB from './NavBottomFAB'; import NavBottomFAB from './NavBottomFAB';
const QUERY_SIZE = 50; const QUERY_SIZE = 50;
const onScroll = ({ y }) => const onScroll = ({ y }: { y: Value<number> }) =>
event( event(
[ [
{ {
@ -32,44 +33,59 @@ const onScroll = ({ y }) =>
{ useNativeDriver: true } { useNativeDriver: true }
); );
class ListContainer extends React.Component { export { IListProps };
static propTypes = {
renderRow: PropTypes.func,
rid: PropTypes.string,
tmid: PropTypes.string,
theme: PropTypes.string,
loading: PropTypes.bool,
listRef: PropTypes.func,
hideSystemMessages: PropTypes.array,
tunread: PropTypes.array,
ignored: PropTypes.array,
navigation: PropTypes.object,
showMessageInMainThread: PropTypes.bool,
serverVersion: PropTypes.string
};
constructor(props) { export interface IListContainerProps {
renderRow: Function;
rid: string;
tmid?: string;
theme: string;
loading: boolean;
listRef: React.RefObject<IListProps>;
hideSystemMessages?: string[];
tunread?: string[];
ignored?: string[];
navigation: any; // TODO: type me
showMessageInMainThread: boolean;
serverVersion: string | null;
}
interface IListContainerState {
messages: TAnyMessageModel[];
refreshing: boolean;
highlightedMessage: string | null;
}
class ListContainer extends React.Component<IListContainerProps, IListContainerState> {
private count = 0;
private mounted = false;
private animated = false;
private jumping = false;
private y = new Value(0);
private onScroll = onScroll({ y: this.y });
private unsubscribeFocus: () => void;
private viewabilityConfig = {
itemVisiblePercentThreshold: 10
};
private highlightedMessageTimeout: number | undefined | false;
private thread?: TThreadModel;
private messagesObservable?: Observable<TMessageModel[] | TThreadMessageModel[]>;
private messagesSubscription?: Subscription;
private viewableItems?: ViewToken[];
constructor(props: IListContainerProps) {
super(props); super(props);
console.time(`${this.constructor.name} init`); console.time(`${this.constructor.name} init`);
console.time(`${this.constructor.name} mount`); console.time(`${this.constructor.name} mount`);
this.count = 0;
this.mounted = false;
this.animated = false;
this.jumping = false;
this.state = { this.state = {
messages: [], messages: [],
refreshing: false, refreshing: false,
highlightedMessage: null highlightedMessage: null
}; };
this.y = new Value(0);
this.onScroll = onScroll({ y: this.y });
this.query(); this.query();
this.unsubscribeFocus = props.navigation.addListener('focus', () => { this.unsubscribeFocus = props.navigation.addListener('focus', () => {
this.animated = true; this.animated = true;
}); });
this.viewabilityConfig = {
itemVisiblePercentThreshold: 10
};
console.timeEnd(`${this.constructor.name} init`); console.timeEnd(`${this.constructor.name} init`);
} }
@ -78,7 +94,7 @@ class ListContainer extends React.Component {
console.timeEnd(`${this.constructor.name} mount`); console.timeEnd(`${this.constructor.name} mount`);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: IListContainerProps, nextState: IListContainerState) {
const { refreshing, highlightedMessage } = this.state; const { refreshing, highlightedMessage } = this.state;
const { hideSystemMessages, theme, tunread, ignored, loading } = this.props; const { hideSystemMessages, theme, tunread, ignored, loading } = this.props;
if (theme !== nextProps.theme) { if (theme !== nextProps.theme) {
@ -105,7 +121,7 @@ class ListContainer extends React.Component {
return false; return false;
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: IListContainerProps) {
const { hideSystemMessages } = this.props; const { hideSystemMessages } = this.props;
if (!dequal(hideSystemMessages, prevProps.hideSystemMessages)) { if (!dequal(hideSystemMessages, prevProps.hideSystemMessages)) {
this.reload(); this.reload();
@ -114,9 +130,6 @@ class ListContainer extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.unsubscribeMessages(); this.unsubscribeMessages();
if (this.onEndReached && this.onEndReached.stop) {
this.onEndReached.stop();
}
if (this.unsubscribeFocus) { if (this.unsubscribeFocus) {
this.unsubscribeFocus(); this.unsubscribeFocus();
} }
@ -158,7 +171,7 @@ class ListContainer extends React.Component {
Q.experimentalSortBy('ts', Q.desc), Q.experimentalSortBy('ts', Q.desc),
Q.experimentalSkip(0), Q.experimentalSkip(0),
Q.experimentalTake(this.count) Q.experimentalTake(this.count)
]; ] as (Q.WhereDescription | Q.Or)[];
if (!showMessageInMainThread) { if (!showMessageInMainThread) {
whereClause.push(Q.or(Q.where('tmid', null), Q.where('tshow', Q.eq(true)))); whereClause.push(Q.or(Q.where('tmid', null), Q.where('tshow', Q.eq(true))));
} }
@ -170,7 +183,7 @@ class ListContainer extends React.Component {
if (rid) { if (rid) {
this.unsubscribeMessages(); this.unsubscribeMessages();
this.messagesSubscription = this.messagesObservable.subscribe(messages => { this.messagesSubscription = this.messagesObservable?.subscribe(messages => {
if (tmid && this.thread) { if (tmid && this.thread) {
messages = [...messages, this.thread]; messages = [...messages, this.thread];
} }
@ -186,6 +199,7 @@ class ListContainer extends React.Component {
if (this.mounted) { if (this.mounted) {
this.setState({ messages }, () => this.update()); this.setState({ messages }, () => this.update());
} else { } else {
// @ts-ignore
this.state.messages = messages; this.state.messages = messages;
} }
// TODO: move it away from here // TODO: move it away from here
@ -246,7 +260,7 @@ class ListContainer extends React.Component {
} }
}; };
getLastMessage = () => { getLastMessage = (): TMessageModel | TThreadMessageModel | null => {
const { messages } = this.state; const { messages } = this.state;
if (messages.length > 0) { if (messages.length > 0) {
return messages[0]; return messages[0];
@ -254,21 +268,23 @@ class ListContainer extends React.Component {
return null; return null;
}; };
handleScrollToIndexFailed = params => { handleScrollToIndexFailed: FlatListProps<any>['onScrollToIndexFailed'] = params => {
const { listRef } = this.props; const { listRef } = this.props;
// @ts-ignore
listRef.current.getNode().scrollToIndex({ index: params.highestMeasuredFrameIndex, animated: false }); listRef.current.getNode().scrollToIndex({ index: params.highestMeasuredFrameIndex, animated: false });
}; };
jumpToMessage = messageId => jumpToMessage = (messageId: string) =>
new Promise(async resolve => { new Promise<void>(async resolve => {
this.jumping = true; this.jumping = true;
const { messages } = this.state; const { messages } = this.state;
const { listRef } = this.props; const { listRef } = this.props;
const index = messages.findIndex(item => item.id === messageId); const index = messages.findIndex(item => item.id === messageId);
if (index > -1) { if (index > -1) {
// @ts-ignore
listRef.current.getNode().scrollToIndex({ index, viewPosition: 0.5, viewOffset: 100 }); listRef.current.getNode().scrollToIndex({ index, viewPosition: 0.5, viewOffset: 100 });
await new Promise(res => setTimeout(res, 300)); await new Promise(res => setTimeout(res, 300));
if (!this.viewableItems.map(vi => vi.key).includes(messageId)) { if (!this.viewableItems?.map(vi => vi.key).includes(messageId)) {
if (!this.jumping) { if (!this.jumping) {
return resolve(); return resolve();
} }
@ -282,6 +298,7 @@ class ListContainer extends React.Component {
}, 10000); }, 10000);
await setTimeout(() => resolve(), 300); await setTimeout(() => resolve(), 300);
} else { } else {
// @ts-ignore
listRef.current.getNode().scrollToIndex({ index: messages.length - 1, animated: false }); listRef.current.getNode().scrollToIndex({ index: messages.length - 1, animated: false });
if (!this.jumping) { if (!this.jumping) {
return resolve(); return resolve();
@ -297,6 +314,7 @@ class ListContainer extends React.Component {
jumpToBottom = () => { jumpToBottom = () => {
const { listRef } = this.props; const { listRef } = this.props;
// @ts-ignore
listRef.current.getNode().scrollToOffset({ offset: -100 }); listRef.current.getNode().scrollToOffset({ offset: -100 });
}; };
@ -308,13 +326,13 @@ class ListContainer extends React.Component {
return null; return null;
}; };
renderItem = ({ item, index }) => { renderItem: FlatListProps<any>['renderItem'] = ({ item, index }) => {
const { messages, highlightedMessage } = this.state; const { messages, highlightedMessage } = this.state;
const { renderRow } = this.props; const { renderRow } = this.props;
return renderRow(item, messages[index + 1], highlightedMessage); return renderRow(item, messages[index + 1], highlightedMessage);
}; };
onViewableItemsChanged = ({ viewableItems }) => { onViewableItemsChanged: FlatListProps<any>['onViewableItemsChanged'] = ({ viewableItems }) => {
this.viewableItems = viewableItems; this.viewableItems = viewableItems;
}; };
@ -325,7 +343,7 @@ class ListContainer extends React.Component {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<> <>
<EmptyRoom rid={rid} length={messages.length} mounted={this.mounted} theme={theme} /> <EmptyRoom rid={rid} length={messages.length} mounted={this.mounted} />
<List <List
onScroll={this.onScroll} onScroll={this.onScroll}
scrollEventThrottle={16} scrollEventThrottle={16}

View File

@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, StyleSheet, Text } from 'react-native'; import { ActivityIndicator, StyleSheet, Text } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad'; import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad';
@ -21,7 +20,7 @@ const styles = StyleSheet.create({
} }
}); });
const LoadMore = ({ load, type, runOnRender }) => { const LoadMore = ({ load, type, runOnRender }: { load: Function; type: string; runOnRender: boolean }): React.ReactElement => {
const { theme } = useTheme(); const { theme } = useTheme();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -62,10 +61,4 @@ const LoadMore = ({ load, type, runOnRender }) => {
); );
}; };
LoadMore.propTypes = {
load: PropTypes.func,
type: PropTypes.string,
runOnRender: PropTypes.bool
};
export default LoadMore; export default LoadMore;

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native'; import { View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
@ -9,34 +8,37 @@ import { isAndroid } from '../../utils/deviceInfo';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import styles from './styles'; import styles from './styles';
import { IApplicationState } from '../../definitions';
const margin = isAndroid ? 40 : 20; const margin = isAndroid ? 40 : 20;
const maxSize = 400; const maxSize = 400;
class ReactionPicker extends React.Component { interface IReactionPickerProps {
static propTypes = { baseUrl: string;
baseUrl: PropTypes.string.isRequired, message?: any;
message: PropTypes.object, show: boolean;
show: PropTypes.bool, isMasterDetail: boolean;
isMasterDetail: PropTypes.bool, reactionClose: () => void;
reactionClose: PropTypes.func, onEmojiSelected: Function; // TODO: properly type this
onEmojiSelected: PropTypes.func, width: number;
width: PropTypes.number, height: number;
height: PropTypes.number, theme: string;
theme: PropTypes.string }
};
shouldComponentUpdate(nextProps) { class ReactionPicker extends React.Component<IReactionPickerProps> {
shouldComponentUpdate(nextProps: IReactionPickerProps) {
const { show, width, height } = this.props; const { show, width, height } = this.props;
return nextProps.show !== show || width !== nextProps.width || height !== nextProps.height; return nextProps.show !== show || width !== nextProps.width || height !== nextProps.height;
} }
onEmojiSelected = (emoji, shortname) => { onEmojiSelected = (emoji: string, shortname: string) => {
// standard emojis: `emoji` is unicode and `shortname` is :joy: // standard emojis: `emoji` is unicode and `shortname` is :joy:
// custom emojis: only `emoji` is returned with shortname type (:joy:) // custom emojis: only `emoji` is returned with shortname type (:joy:)
// to set reactions, we need shortname type // to set reactions, we need shortname type
const { onEmojiSelected, message } = this.props; const { onEmojiSelected, message } = this.props;
onEmojiSelected(shortname || emoji, message.id); if (message) {
onEmojiSelected(shortname || emoji, message.id);
}
}; };
render() { render() {
@ -79,7 +81,7 @@ class ReactionPicker extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: IApplicationState) => ({
baseUrl: state.server.server, baseUrl: state.server.server,
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });

View File

@ -1,30 +1,43 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { Observable, Subscription } from 'rxjs';
import { StackNavigationProp } from '@react-navigation/stack';
import * as HeaderButton from '../../containers/HeaderButton'; import * as HeaderButton from '../../containers/HeaderButton';
import database from '../../lib/database'; import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import { events, logEvent } from '../../utils/log'; import { events, logEvent } from '../../utils/log';
import { isTeamRoom } from '../../utils/room'; import { isTeamRoom } from '../../utils/room';
import { IApplicationState, SubscriptionType, TMessageModel, TSubscriptionModel } from '../../definitions';
import { ChatsStackParamList } from '../../stacks/types';
class RightButtonsContainer extends Component { interface IRightButtonsProps {
static propTypes = { userId?: string;
userId: PropTypes.string, threadsEnabled: boolean;
threadsEnabled: PropTypes.bool, rid?: string;
rid: PropTypes.string, t: string;
t: PropTypes.string, tmid?: string;
tmid: PropTypes.string, teamId?: string;
teamId: PropTypes.string, isMasterDetail: boolean;
navigation: PropTypes.object, toggleFollowThread: Function;
isMasterDetail: PropTypes.bool, joined: boolean;
toggleFollowThread: PropTypes.func, encrypted?: boolean;
joined: PropTypes.bool, navigation: StackNavigationProp<ChatsStackParamList, 'RoomView'>;
encrypted: PropTypes.bool }
};
constructor(props) { interface IRigthButtonsState {
isFollowingThread: boolean;
tunread: string[];
tunreadUser: string[];
tunreadGroup: string[];
}
class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsState> {
private threadSubscription?: Subscription;
private subSubscription?: Subscription;
constructor(props: IRightButtonsProps) {
super(props); super(props);
this.state = { this.state = {
isFollowingThread: true, isFollowingThread: true,
@ -56,7 +69,7 @@ class RightButtonsContainer extends Component {
} }
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: IRightButtonsProps, nextState: IRigthButtonsState) {
const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state; const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state;
const { teamId } = this.props; const { teamId } = this.props;
if (nextProps.teamId !== teamId) { if (nextProps.teamId !== teamId) {
@ -86,37 +99,41 @@ class RightButtonsContainer extends Component {
} }
} }
observeThread = threadRecord => { observeThread = (threadRecord: TMessageModel) => {
const threadObservable = threadRecord.observe(); const threadObservable: Observable<TMessageModel> = threadRecord.observe();
this.threadSubscription = threadObservable.subscribe(thread => this.updateThread(thread)); this.threadSubscription = threadObservable.subscribe(thread => this.updateThread(thread));
}; };
updateThread = thread => { updateThread = (thread: TMessageModel) => {
const { userId } = this.props; const { userId } = this.props;
this.setState({ this.setState({
isFollowingThread: thread.replies && !!thread.replies.find(t => t === userId) isFollowingThread: (thread.replies && !!thread.replies.find(t => t === userId)) ?? false
}); });
}; };
observeSubscription = subRecord => { observeSubscription = (subRecord: TSubscriptionModel) => {
const subObservable = subRecord.observe(); const subObservable = subRecord.observe();
this.subSubscription = subObservable.subscribe(sub => { this.subSubscription = subObservable.subscribe(sub => {
this.updateSubscription(sub); this.updateSubscription(sub);
}); });
}; };
updateSubscription = sub => { updateSubscription = (sub: TSubscriptionModel) => {
this.setState({ this.setState({
tunread: sub?.tunread, tunread: sub?.tunread ?? [],
tunreadUser: sub?.tunreadUser, tunreadUser: sub?.tunreadUser ?? [],
tunreadGroup: sub?.tunreadGroup tunreadGroup: sub?.tunreadGroup ?? []
}); });
}; };
goTeamChannels = () => { goTeamChannels = () => {
logEvent(events.ROOM_GO_TEAM_CHANNELS); logEvent(events.ROOM_GO_TEAM_CHANNELS);
const { navigation, isMasterDetail, teamId } = this.props; const { navigation, isMasterDetail, teamId } = this.props;
if (!teamId) {
return;
}
if (isMasterDetail) { if (isMasterDetail) {
// @ts-ignore TODO: find a way to make this work
navigation.navigate('ModalStackNavigator', { navigation.navigate('ModalStackNavigator', {
screen: 'TeamChannelsView', screen: 'TeamChannelsView',
params: { teamId } params: { teamId }
@ -129,23 +146,31 @@ class RightButtonsContainer extends Component {
goThreadsView = () => { goThreadsView = () => {
logEvent(events.ROOM_GO_THREADS); logEvent(events.ROOM_GO_THREADS);
const { rid, t, navigation, isMasterDetail } = this.props; const { rid, t, navigation, isMasterDetail } = this.props;
if (!rid) {
return;
}
if (isMasterDetail) { if (isMasterDetail) {
// @ts-ignore TODO: find a way to make this work
navigation.navigate('ModalStackNavigator', { screen: 'ThreadMessagesView', params: { rid, t } }); navigation.navigate('ModalStackNavigator', { screen: 'ThreadMessagesView', params: { rid, t } });
} else { } else {
navigation.navigate('ThreadMessagesView', { rid, t }); navigation.navigate('ThreadMessagesView', { rid, t: t as SubscriptionType });
} }
}; };
goSearchView = () => { goSearchView = () => {
logEvent(events.ROOM_GO_SEARCH); logEvent(events.ROOM_GO_SEARCH);
const { rid, t, navigation, isMasterDetail, encrypted } = this.props; const { rid, t, navigation, isMasterDetail, encrypted } = this.props;
if (!rid) {
return;
}
if (isMasterDetail) { if (isMasterDetail) {
// @ts-ignore TODO: find a way to make this work
navigation.navigate('ModalStackNavigator', { navigation.navigate('ModalStackNavigator', {
screen: 'SearchMessagesView', screen: 'SearchMessagesView',
params: { rid, showCloseModal: true, encrypted } params: { rid, showCloseModal: true, encrypted }
}); });
} else { } else {
navigation.navigate('SearchMessagesView', { rid, t, encrypted }); navigation.navigate('SearchMessagesView', { rid, t: t as SubscriptionType, encrypted });
} }
}; };
@ -194,9 +219,9 @@ class RightButtonsContainer extends Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: IApplicationState) => ({
userId: getUserSelector(state).id, userId: getUserSelector(state).id,
threadsEnabled: state.settings.Threads_enabled, threadsEnabled: state.settings.Threads_enabled as boolean,
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });

View File

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import I18n from '../../i18n'; import I18n from '../../i18n';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { useTheme } from '../../theme';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -34,7 +34,8 @@ const styles = StyleSheet.create({
} }
}); });
const DateSeparator = React.memo(({ ts, unread, theme }) => { const DateSeparator = ({ ts, unread }: { ts: Date | string | null; unread: boolean }): React.ReactElement => {
const { theme } = useTheme();
const date = ts ? moment(ts).format('LL') : null; const date = ts ? moment(ts).format('LL') : null;
const unreadLine = { backgroundColor: themes[theme].dangerColor }; const unreadLine = { backgroundColor: themes[theme].dangerColor };
const unreadText = { color: themes[theme].dangerColor }; const unreadText = { color: themes[theme].dangerColor };
@ -61,12 +62,6 @@ const DateSeparator = React.memo(({ ts, unread, theme }) => {
<View style={[styles.line, unreadLine]} /> <View style={[styles.line, unreadLine]} />
</View> </View>
); );
});
DateSeparator.propTypes = {
ts: PropTypes.instanceOf(Date),
unread: PropTypes.bool,
theme: PropTypes.string
}; };
export default DateSeparator; export default DateSeparator;

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import PropTypes from 'prop-types';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { Observable, Subscription } from 'rxjs';
import database from '../../lib/database'; import database from '../../lib/database';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
@ -11,6 +11,7 @@ import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { IUser, TUploadModel } from '../../definitions';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -51,23 +52,26 @@ const styles = StyleSheet.create({
} }
}); });
class UploadProgress extends Component { interface IUploadProgressProps {
static propTypes = { width: number;
width: PropTypes.number, rid: string;
rid: PropTypes.string, user: Pick<IUser, 'id' | 'username' | 'token'>;
theme: PropTypes.string, baseUrl: string;
user: PropTypes.shape({ theme?: string;
id: PropTypes.string.isRequired, }
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
baseUrl: PropTypes.string.isRequired
};
constructor(props) { interface IUploadProgressState {
uploads: TUploadModel[];
}
class UploadProgress extends Component<IUploadProgressProps, IUploadProgressState> {
private mounted = false;
private ranInitialUploadCheck = false;
private uploadsObservable?: Observable<TUploadModel[]>;
private uploadsSubscription?: Subscription;
constructor(props: IUploadProgressProps) {
super(props); super(props);
this.mounted = false;
this.ranInitialUploadCheck = false;
this.state = { this.state = {
uploads: [] uploads: []
}; };
@ -97,6 +101,7 @@ class UploadProgress extends Component {
if (this.mounted) { if (this.mounted) {
this.setState({ uploads }); this.setState({ uploads });
} else { } else {
// @ts-ignore
this.state.uploads = uploads; this.state.uploads = uploads;
} }
if (!this.ranInitialUploadCheck) { if (!this.ranInitialUploadCheck) {
@ -112,7 +117,7 @@ class UploadProgress extends Component {
if (!RocketChat.isUploadActive(u.path)) { if (!RocketChat.isUploadActive(u.path)) {
try { try {
const db = database.active; const db = database.active;
await db.action(async () => { await db.write(async () => {
await u.update(() => { await u.update(() => {
u.error = true; u.error = true;
}); });
@ -124,10 +129,10 @@ class UploadProgress extends Component {
}); });
}; };
deleteUpload = async item => { deleteUpload = async (item: TUploadModel) => {
try { try {
const db = database.active; const db = database.active;
await db.action(async () => { await db.write(async () => {
await item.destroyPermanently(); await item.destroyPermanently();
}); });
} catch (e) { } catch (e) {
@ -135,7 +140,7 @@ class UploadProgress extends Component {
} }
}; };
cancelUpload = async item => { cancelUpload = async (item: TUploadModel) => {
try { try {
await RocketChat.cancelUpload(item); await RocketChat.cancelUpload(item);
} catch (e) { } catch (e) {
@ -143,12 +148,12 @@ class UploadProgress extends Component {
} }
}; };
tryAgain = async item => { tryAgain = async (item: TUploadModel) => {
const { rid, baseUrl: server, user } = this.props; const { rid, baseUrl: server, user } = this.props;
try { try {
const db = database.active; const db = database.active;
await db.action(async () => { await db.write(async () => {
await item.update(() => { await item.update(() => {
item.error = false; item.error = false;
}); });
@ -159,44 +164,44 @@ class UploadProgress extends Component {
} }
}; };
renderItemContent = item => { renderItemContent = (item: TUploadModel) => {
const { width, theme } = this.props; const { width, theme } = this.props;
if (!item.error) { if (!item.error) {
return [ return [
<View key='row' style={styles.row}> <View key='row' style={styles.row}>
<CustomIcon name='attach' size={20} color={themes[theme].auxiliaryText} /> <CustomIcon name='attach' size={20} color={themes[theme!].auxiliaryText} />
<Text <Text
style={[styles.descriptionContainer, styles.descriptionText, { color: themes[theme].auxiliaryText }]} style={[styles.descriptionContainer, styles.descriptionText, { color: themes[theme!].auxiliaryText }]}
numberOfLines={1}> numberOfLines={1}>
{I18n.t('Uploading')} {item.name} {I18n.t('Uploading')} {item.name}
</Text> </Text>
<CustomIcon name='close' size={20} color={themes[theme].auxiliaryText} onPress={() => this.cancelUpload(item)} /> <CustomIcon name='close' size={20} color={themes[theme!].auxiliaryText} onPress={() => this.cancelUpload(item)} />
</View>, </View>,
<View <View
key='progress' key='progress'
style={[styles.progress, { width: (width * item.progress) / 100, backgroundColor: themes[theme].tintColor }]} style={[styles.progress, { width: (width * (item.progress ?? 0)) / 100, backgroundColor: themes[theme!].tintColor }]}
/> />
]; ];
} }
return ( return (
<View style={styles.row}> <View style={styles.row}>
<CustomIcon name='warning' size={20} color={themes[theme].dangerColor} /> <CustomIcon name='warning' size={20} color={themes[theme!].dangerColor} />
<View style={styles.descriptionContainer}> <View style={styles.descriptionContainer}>
<Text style={[styles.descriptionText, { color: themes[theme].auxiliaryText }]} numberOfLines={1}> <Text style={[styles.descriptionText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
{I18n.t('Error_uploading')} {item.name} {I18n.t('Error_uploading')} {item.name}
</Text> </Text>
<TouchableOpacity onPress={() => this.tryAgain(item)}> <TouchableOpacity onPress={() => this.tryAgain(item)}>
<Text style={[styles.tryAgainButtonText, { color: themes[theme].tintColor }]}>{I18n.t('Try_again')}</Text> <Text style={[styles.tryAgainButtonText, { color: themes[theme!].tintColor }]}>{I18n.t('Try_again')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<CustomIcon name='close' size={20} color={themes[theme].auxiliaryText} onPress={() => this.deleteUpload(item)} /> <CustomIcon name='close' size={20} color={themes[theme!].auxiliaryText} onPress={() => this.deleteUpload(item)} />
</View> </View>
); );
}; };
// TODO: transform into stateless and update based on its own observable changes // TODO: transform into stateless and update based on its own observable changes
renderItem = (item, index) => { renderItem = (item: TUploadModel, index: number) => {
const { theme } = this.props; const { theme } = this.props;
return ( return (
@ -206,8 +211,8 @@ class UploadProgress extends Component {
styles.item, styles.item,
index !== 0 ? { marginTop: 10 } : {}, index !== 0 ? { marginTop: 10 } : {},
{ {
backgroundColor: themes[theme].chatComponentBackground, backgroundColor: themes[theme!].chatComponentBackground,
borderColor: themes[theme].borderColor borderColor: themes[theme!].borderColor
} }
]}> ]}>
{this.renderItemContent(item)} {this.renderItemContent(item)}

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +0,0 @@
import { getMessageById } from '../../../lib/database/services/Message';
import { getThreadMessageById } from '../../../lib/database/services/ThreadMessage';
import getSingleMessage from '../../../lib/methods/getSingleMessage';
const getMessageInfo = async messageId => {
let result;
result = await getMessageById(messageId);
if (result) {
return {
id: result.id,
rid: result.subscription.id,
tmid: result.tmid,
msg: result.msg
};
}
result = await getThreadMessageById(messageId);
if (result) {
return {
id: result.id,
rid: result.subscription.id,
tmid: result.rid,
msg: result.msg
};
}
result = await getSingleMessage(messageId);
if (result) {
return {
id: result._id,
rid: result.rid,
tmid: result.tmid,
msg: result.msg,
fromServer: true
};
}
return null;
};
export default getMessageInfo;

View File

@ -0,0 +1,41 @@
import { TMessageModel, TThreadMessageModel } from '../../../definitions';
import { getMessageById } from '../../../lib/database/services/Message';
import { getThreadMessageById } from '../../../lib/database/services/ThreadMessage';
import getSingleMessage from '../../../lib/methods/getSingleMessage';
const getMessageInfo = async (messageId: string): Promise<TMessageModel | TThreadMessageModel | any | null> => {
const message = await getMessageById(messageId);
if (message) {
return {
id: message.id,
rid: message?.subscription?.id,
tmid: message.tmid,
msg: message.msg
};
}
const threadMessage = await getThreadMessageById(messageId);
if (threadMessage) {
return {
id: threadMessage.id,
rid: threadMessage?.subscription?.id,
tmid: threadMessage.rid,
msg: threadMessage.msg
};
}
const singleMessage: any = await getSingleMessage(messageId);
if (singleMessage) {
return {
id: singleMessage._id,
rid: singleMessage.rid,
tmid: singleMessage.tmid,
msg: singleMessage.msg,
fromServer: true
};
}
return null;
};
export default getMessageInfo;

View File

@ -1,10 +0,0 @@
import RocketChat from '../../../lib/rocketchat';
const getMessages = room => {
if (room.lastOpen) {
return RocketChat.loadMissedMessages(room);
} else {
return RocketChat.loadMessagesForRoom(room);
}
};
export default getMessages;

View File

@ -0,0 +1,23 @@
import loadMessagesForRoom from '../../../lib/methods/loadMessagesForRoom';
import loadMissedMessages from '../../../lib/methods/loadMissedMessages';
// TODO: clarify latest vs lastOpen
const getMessages = ({
rid,
t,
latest,
lastOpen,
loaderItem
}: {
rid: string;
t?: string;
latest?: Date;
lastOpen?: Date;
loaderItem?: any; // TODO: type this
}): Promise<void> => {
if (lastOpen) {
return loadMissedMessages({ rid, lastOpen });
}
return loadMessagesForRoom({ rid, t: t as any, latest, loaderItem });
};
export default getMessages;

View File

@ -1,27 +0,0 @@
import {
MESSAGE_TYPE_LOAD_MORE,
MESSAGE_TYPE_LOAD_NEXT_CHUNK,
MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK
} from '../../../constants/messageTypeLoad';
import RocketChat from '../../../lib/rocketchat';
const getMoreMessages = ({ rid, t, tmid, loaderItem }) => {
if ([MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK].includes(loaderItem.t)) {
return RocketChat.loadMessagesForRoom({
rid,
t,
latest: loaderItem.ts,
loaderItem
});
}
if (loaderItem.t === MESSAGE_TYPE_LOAD_NEXT_CHUNK) {
return RocketChat.loadNextMessages({
rid,
tmid,
ts: loaderItem.ts,
loaderItem
});
}
};
export default getMoreMessages;

View File

@ -0,0 +1,40 @@
import { MessageType, 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';
const getMoreMessages = ({
rid,
t,
tmid,
loaderItem
}: {
rid: string;
t: SubscriptionType;
tmid?: string;
loaderItem: TAnyMessageModel;
}): Promise<void> => {
if ([MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK].includes(loaderItem.t as MessageType)) {
return loadMessagesForRoom({
rid,
t: t as any,
latest: loaderItem.ts as Date,
loaderItem
});
}
if (loaderItem.t === MESSAGE_TYPE_LOAD_NEXT_CHUNK) {
return loadNextMessages({
rid,
tmid,
ts: loaderItem.ts as Date,
loaderItem
});
}
return Promise.resolve();
};
export default getMoreMessages;

View File

@ -1,6 +1,6 @@
import RocketChat from '../../../lib/rocketchat'; import RocketChat from '../../../lib/rocketchat';
// unlike getMessages, sync isn't required for threads, because loadMissedMessages does it already // unlike getMessages, sync isn't required for threads, because loadMissedMessages does it already
const getThreadMessages = (tmid, rid) => RocketChat.loadThreadMessages({ tmid, rid }); const getThreadMessages = (tmid: string, rid: string): Promise<void> => RocketChat.loadThreadMessages({ tmid, rid });
export default getThreadMessages; export default getThreadMessages;

View File

@ -1,5 +0,0 @@
import RocketChat from '../../../lib/rocketchat';
const readMessages = (rid, newLastOpen) => RocketChat.readMessages(rid, newLastOpen, true);
export default readMessages;

View File

@ -0,0 +1,5 @@
import RocketChat from '../../../lib/rocketchat';
const readMessages = (rid: string, newLastOpen: Date): Promise<void> => RocketChat.readMessages(rid, newLastOpen, true);
export default readMessages;

View File

@ -257,6 +257,7 @@ class SearchMessagesView extends React.Component<ISearchMessagesViewProps, ISear
const { user, baseUrl, theme, useRealName } = this.props; const { user, baseUrl, theme, useRealName } = this.props;
return ( return (
<Message <Message
// @ts-ignore IMessage | TMessageModel?
item={item} item={item}
baseUrl={baseUrl} baseUrl={baseUrl}
user={user} user={user}

View File

@ -29,7 +29,7 @@ const styles = StyleSheet.create({
}); });
interface ISelectListViewState { interface ISelectListViewState {
data: IRoom[]; data?: IRoom[];
dataFiltered?: IRoom[]; dataFiltered?: IRoom[];
isSearching: boolean; isSearching: boolean;
selected: string[]; selected: string[];
@ -53,7 +53,7 @@ class SelectListView extends React.Component<ISelectListViewProps, ISelectListVi
private isSearch: boolean; private isSearch: boolean;
private onSearch: (text: string) => Partial<IRoom[]>; private onSearch?: (text: string) => Promise<Partial<IRoom[]> | any>;
private isRadio?: boolean; private isRadio?: boolean;
@ -61,10 +61,10 @@ class SelectListView extends React.Component<ISelectListViewProps, ISelectListVi
super(props); super(props);
const data = props.route?.params?.data; const data = props.route?.params?.data;
this.title = props.route?.params?.title; this.title = props.route?.params?.title;
this.infoText = props.route?.params?.infoText; this.infoText = props.route?.params?.infoText ?? '';
this.nextAction = props.route?.params?.nextAction; this.nextAction = props.route?.params?.nextAction;
this.showAlert = props.route?.params?.showAlert; this.showAlert = props.route?.params?.showAlert ?? (() => {});
this.isSearch = props.route?.params?.isSearch; this.isSearch = props.route?.params?.isSearch ?? false;
this.onSearch = props.route?.params?.onSearch; this.onSearch = props.route?.params?.onSearch;
this.isRadio = props.route?.params?.isRadio; this.isRadio = props.route?.params?.isRadio;
this.state = { this.state = {
@ -122,7 +122,7 @@ class SelectListView extends React.Component<ISelectListViewProps, ISelectListVi
search = async (text: string) => { search = async (text: string) => {
try { try {
this.setState({ isSearching: true }); this.setState({ isSearching: true });
const result = (await this.onSearch(text)) as IRoom[]; const result = (await this.onSearch?.(text)) as IRoom[];
this.setState({ dataFiltered: result }); this.setState({ dataFiltered: result });
} catch (e) { } catch (e) {
log(e); log(e);

View File

@ -28,8 +28,7 @@ import Preview from './Preview';
import Header from './Header'; import Header from './Header';
import styles from './styles'; import styles from './styles';
import { IAttachment } from './interfaces'; import { IAttachment } from './interfaces';
import { ISubscription } from '../../definitions/ISubscription'; import { IUser, TSubscriptionModel } from '../../definitions';
import { IUser } from '../../definitions';
interface IShareViewState { interface IShareViewState {
selected: IAttachment; selected: IAttachment;
@ -37,7 +36,7 @@ interface IShareViewState {
readOnly: boolean; readOnly: boolean;
attachments: IAttachment[]; attachments: IAttachment[];
text: string; text: string;
room: ISubscription; room: TSubscriptionModel;
thread: any; // change thread: any; // change
maxFileSize: number; maxFileSize: number;
mediaAllowList: string; mediaAllowList: string;
@ -153,7 +152,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> {
getReadOnly = async () => { getReadOnly = async () => {
const { room } = this.state; const { room } = this.state;
const { user } = this.props; const { user } = this.props;
const readOnly = await isReadOnly(room, user); const readOnly = await isReadOnly(room, user.username);
return readOnly; return readOnly;
}; };

View File

@ -70,6 +70,7 @@ const Item = ({ item, useRealName, user, badgeColor, onPress, toggleFollowThread
const username = (useRealName && item?.u?.name) || item?.u?.username; const username = (useRealName && item?.u?.name) || item?.u?.username;
let time; let time;
if (item?.ts) { if (item?.ts) {
// @ts-ignore TODO: to be fixed after we unify our types
time = formatDateThreads(item.ts); time = formatDateThreads(item.ts);
} }

View File

@ -215,9 +215,6 @@ describe('Discussion', () => {
await waitFor(element(by.id(`room-view-title-${discussionName}`))) await waitFor(element(by.id(`room-view-title-${discussionName}`)))
.toExist() .toExist()
.withTimeout(5000); .withTimeout(5000);
await waitFor(element(by.id('messagebox')))
.toBeVisible()
.withTimeout(60000);
}); });
}); });
}); });

View File

@ -5,6 +5,9 @@ const toBeConverted = `to-be-converted-${data.random}`;
const toBeMoved = `to-be-moved-${data.random}`; const toBeMoved = `to-be-moved-${data.random}`;
const createChannel = async room => { const createChannel = async room => {
await waitFor(element(by.id('rooms-list-view-create-channel')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('rooms-list-view-create-channel')).tap(); await element(by.id('rooms-list-view-create-channel')).tap();
await waitFor(element(by.id('new-message-view'))) await waitFor(element(by.id('new-message-view')))
.toExist() .toExist()

View File

@ -1682,7 +1682,7 @@
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 4.25.0; MARKETING_VERSION = 4.26.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
@ -1719,7 +1719,7 @@
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 4.25.0; MARKETING_VERSION = 4.26.0;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";

View File

@ -26,7 +26,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.25.0</string> <string>4.26.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>

View File

@ -26,7 +26,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>4.25.0</string> <string>4.26.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>KeychainGroup</key> <key>KeychainGroup</key>

View File

@ -1,6 +1,6 @@
{ {
"name": "rocket-chat-reactnative", "name": "rocket-chat-reactnative",
"version": "4.25.0", "version": "4.26.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "react-native start", "start": "react-native start",