Merge branch 'develop' into chore.migrate-redux-server-to-ts

This commit is contained in:
Gerzon Z 2022-02-01 11:27:57 -04:00
commit f8a37fe082
82 changed files with 905 additions and 212 deletions

View File

@ -359,7 +359,7 @@ jobs:
- run: - run:
name: Test name: Test
command: | command: |
yarn test yarn test -w 8
- run: - run:
name: Codecov name: Codecov

View File

@ -22,18 +22,13 @@ global.Date.now = jest.fn(() => new Date('2019-10-10').getTime());
const converter = new Stories2SnapsConverter(); const converter = new Stories2SnapsConverter();
// Runner
initStoryshots({ initStoryshots({
asyncJest: true, test: ({ story, context }) => {
test: ({ story, context, done }) => {
const snapshotFilename = converter.getSnapshotFileName(context); const snapshotFilename = converter.getSnapshotFileName(context);
const storyElement = story.render(); const storyElement = story.render();
const { update, toJSON } = render(storyElement); const { update, toJSON } = render(storyElement);
update(storyElement); update(storyElement);
setTimeout(() => {
const json = toJSON(); const json = toJSON();
expect(JSON.stringify(json)).toMatchSpecificSnapshot(snapshotFilename); expect(JSON.stringify(json)).toMatchSpecificSnapshot(snapshotFilename);
done();
}, 10);
} }
}); });

View File

@ -1,16 +1,18 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
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 { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import { TSubscriptionModel, TUserModel } from '../../definitions';
import Avatar from './Avatar'; import Avatar from './Avatar';
import { IAvatar } from './interfaces'; import { IAvatar } from './interfaces';
class AvatarContainer extends React.Component<IAvatar, any> { class AvatarContainer extends React.Component<IAvatar, any> {
private mounted: boolean; private mounted: boolean;
private subscription: any; private subscription?: Subscription;
static defaultProps = { static defaultProps = {
text: '', text: '',
@ -59,15 +61,17 @@ class AvatarContainer extends React.Component<IAvatar, any> {
record = user; record = user;
} else { } else {
const { rid } = this.props; const { rid } = this.props;
if (rid) {
record = await subsCollection.find(rid); record = await subsCollection.find(rid);
} }
}
} catch { } catch {
// Record not found // Record not found
} }
if (record) { if (record) {
const observable = record.observe(); const observable = record.observe() as Observable<TSubscriptionModel | TUserModel>;
this.subscription = observable.subscribe((r: any) => { this.subscription = observable.subscribe(r => {
const { avatarETag } = r; const { avatarETag } = r;
if (this.mounted) { if (this.mounted) {
this.setState({ avatarETag }); this.setState({ avatarETag });

View File

@ -35,7 +35,7 @@ const styles = StyleSheet.create({
const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => ( const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => (
<View style={styles.container}> <View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} /> <ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
{text ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null} {text && !loading ? <Text style={[styles.text, { color: themes[theme!].auxiliaryTintColor }]}>{text}</Text> : null}
{loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null} {loading ? <ActivityIndicator style={styles.text} color={themes[theme!].auxiliaryTintColor} /> : null}
</View> </View>
); );

View File

@ -36,7 +36,7 @@ interface IEmojiPickerProps {
} }
interface IEmojiPickerState { interface IEmojiPickerState {
frequentlyUsed: []; frequentlyUsed: (string | { content?: string; extension?: string; isCustom: boolean })[];
customEmojis: any; customEmojis: any;
show: boolean; show: boolean;
width: number | null; width: number | null;
@ -114,7 +114,7 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
// Do nothing // Do nothing
} }
await db.action(async () => { await db.write(async () => {
if (freqEmojiRecord) { if (freqEmojiRecord) {
await freqEmojiRecord.update((f: any) => { await freqEmojiRecord.update((f: any) => {
f.count += 1; f.count += 1;
@ -132,8 +132,8 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
updateFrequentlyUsed = async () => { updateFrequentlyUsed = async () => {
const db = database.active; const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch(); const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
let frequentlyUsed: any = orderBy(frequentlyUsedRecords, ['count'], ['desc']); const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
frequentlyUsed = frequentlyUsed.map((item: IEmoji) => { const frequentlyUsed = frequentlyUsedOrdered.map(item => {
if (item.isCustom) { if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom }; return { content: item.content, extension: item.extension, isCustom: item.isCustom };
} }

View File

@ -10,6 +10,7 @@ import database from '../../lib/database';
import { Button } from '../ActionSheet'; import { Button } from '../ActionSheet';
import { useDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { TFrequentlyUsedEmojiModel } from '../../definitions/IFrequentlyUsedEmoji';
import { IEmoji } from '../EmojiPicker/interfaces'; import { IEmoji } from '../EmojiPicker/interfaces';
interface IHeader { interface IHeader {
@ -90,14 +91,14 @@ const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => (
)); ));
const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => { const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => {
const [items, setItems] = useState([]); const [items, setItems] = useState<(TFrequentlyUsedEmojiModel | string)[]>([]);
const { width, height }: any = useDimensions(); const { width, height }: any = useDimensions();
const setEmojis = async () => { const setEmojis = async () => {
try { try {
const db = database.active; const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis'); const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis = await freqEmojiCollection.query().fetch(); let freqEmojis: (TFrequentlyUsedEmojiModel | string)[] = await freqEmojiCollection.query().fetch();
const isLandscape = width > height; const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2; const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;

View File

@ -15,6 +15,7 @@ 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';
interface IMessageActions { interface IMessageActions {
room: { room: {
@ -182,9 +183,9 @@ const MessageActions = React.memo(
if (result.success) { if (result.success) {
const subCollection = db.get('subscriptions'); const subCollection = db.get('subscriptions');
const subRecord = await subCollection.find(rid); const subRecord = await subCollection.find(rid);
await db.action(async () => { await db.write(async () => {
try { try {
await subRecord.update((sub: any) => (sub.lastOpen = ts)); await subRecord.update(sub => (sub.lastOpen = ts));
} catch { } catch {
// do nothing // do nothing
} }
@ -269,11 +270,11 @@ const MessageActions = React.memo(
} }
}; };
const handleToggleTranslation = async (message: any) => { const handleToggleTranslation = async (message: TMessageModel) => {
try { try {
const db = database.active; const db = database.active;
await db.action(async () => { await db.write(async () => {
await message.update((m: any) => { await message.update(m => {
m.autoTranslate = !m.autoTranslate; m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date(); m._updatedAt = new Date();
}); });
@ -320,7 +321,7 @@ const MessageActions = React.memo(
}); });
}; };
const getOptions = (message: any) => { const getOptions = (message: TMessageModel) => {
let options: any = []; let options: any = [];
// Reply // Reply
@ -446,7 +447,7 @@ const MessageActions = React.memo(
return options; return options;
}; };
const showMessageActions = async (message: any) => { const showMessageActions = async (message: TMessageModel) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS); logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions(); await getPermissions();
showActionSheet({ showActionSheet({

View File

@ -36,7 +36,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
try { try {
// Find the thread header and update it // Find the thread header and update it
const msg = await msgCollection.find(tmid); const msg = await msgCollection.find(tmid);
if (msg.tcount <= 1) { if (msg?.tcount && msg.tcount <= 1) {
deleteBatch.push( deleteBatch.push(
msg.prepareUpdate((m: any) => { msg.prepareUpdate((m: any) => {
m.tcount = null; m.tcount = null;
@ -62,7 +62,7 @@ const MessageErrorActions = forwardRef(({ tmid }: any, ref): any => {
// Do nothing: message not found // Do nothing: message not found
} }
} }
await db.action(async () => { await db.write(async () => {
await db.batch(...deleteBatch); await db.batch(...deleteBatch);
}); });
} catch (e) { } catch (e) {

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { withTheme } from '../theme'; import I18n from '../i18n';
import { useTheme } from '../theme';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import TextInput from '../presentation/TextInput'; import TextInput from '../presentation/TextInput';
@ -19,14 +20,13 @@ const styles = StyleSheet.create({
} }
}); });
interface ISearchHeader { interface ISearchHeaderProps {
theme?: string;
onSearchChangeText?: (text: string) => void; onSearchChangeText?: (text: string) => void;
testID: string;
} }
// TODO: it might be useful to refactor this component for reusage const SearchHeader = ({ onSearchChangeText, testID }: ISearchHeaderProps): JSX.Element => {
const SearchHeader = ({ theme, onSearchChangeText }: ISearchHeader) => { const { theme } = useTheme();
const titleColorStyle = { color: themes[theme!].headerTitleColor };
const isLight = theme === 'light'; const isLight = theme === 'light';
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1; const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1;
@ -36,14 +36,14 @@ const SearchHeader = ({ theme, onSearchChangeText }: ISearchHeader) => {
<View style={styles.container}> <View style={styles.container}>
<TextInput <TextInput
autoFocus autoFocus
style={[styles.title, isLight && titleColorStyle, { fontSize: titleFontSize }]} style={[styles.title, isLight && { color: themes[theme].headerTitleColor }, { fontSize: titleFontSize }]}
placeholder='Search' placeholder={I18n.t('Search')}
onChangeText={onSearchChangeText} onChangeText={onSearchChangeText}
theme={theme!} theme={theme}
testID='thread-messages-view-search-header' testID={testID}
/> />
</View> </View>
); );
}; };
export default withTheme(SearchHeader); export default SearchHeader;

View File

@ -5,7 +5,7 @@ import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { withTheme } from '../theme'; import { useTheme } from '../theme';
import { TThreadModel } from '../definitions/IThread'; import { TThreadModel } from '../definitions/IThread';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -48,12 +48,12 @@ interface IThreadDetails {
badgeColor?: string; badgeColor?: string;
toggleFollowThread: Function; toggleFollowThread: Function;
style: ViewStyle; style: ViewStyle;
theme?: string;
} }
const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, theme }: IThreadDetails) => { const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IThreadDetails): JSX.Element => {
const { theme } = useTheme();
let { tcount } = item; let { tcount } = item;
if (tcount! >= 1000) { if (tcount && tcount >= 1000) {
tcount = '+999'; tcount = '+999';
} }
@ -81,7 +81,6 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, them
</Text> </Text>
</View> </View>
</View> </View>
<View style={styles.badgeContainer}> <View style={styles.badgeContainer}>
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null} {badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null}
<Touchable onPress={() => toggleFollowThread?.(isFollowing, item.id)}> <Touchable onPress={() => toggleFollowThread?.(isFollowing, item.id)}>
@ -96,4 +95,4 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, them
); );
}; };
export default withTheme(ThreadDetails); export default ThreadDetails;

View File

@ -25,7 +25,7 @@ import { isValidURL } from '../../utils/url';
import NewMarkdown from './new'; import NewMarkdown from './new';
interface IMarkdownProps { interface IMarkdownProps {
msg: string; msg?: string;
md: MarkdownAST; md: MarkdownAST;
mentions: UserMention[]; mentions: UserMention[];
getCustomEmoji: Function; getCustomEmoji: Function;

View File

@ -182,7 +182,7 @@ const Fields = React.memo(
<Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text> <Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text>
{/* @ts-ignore*/} {/* @ts-ignore*/}
<Markdown <Markdown
msg={field.value!} msg={field?.value || ''}
baseUrl={baseUrl} baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}

View File

@ -24,7 +24,6 @@ const Thread = React.memo(
item={{ item={{
tcount, tcount,
replies, replies,
tlm,
id id
}} }}
user={user} user={user}

View File

@ -147,6 +147,12 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
if ((item.tlm || item.tmid) && !isThreadRoom) { if ((item.tlm || item.tmid) && !isThreadRoom) {
this.onThreadPress(); this.onThreadPress();
} }
const { onDiscussionPress } = this.props;
if (onDiscussionPress) {
onDiscussionPress(item);
}
}, },
300, 300,
true true

View File

@ -1,5 +1,7 @@
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
export type TMessageType = 'discussion-created' | 'jitsi_call_started';
export interface IMessageAttachments { export interface IMessageAttachments {
attachments: any; attachments: any;
timeFormat: string; timeFormat: string;
@ -101,7 +103,7 @@ export interface IMessageThread {
msg: string; msg: string;
tcount: number; tcount: number;
theme: string; theme: string;
tlm: string; tlm: Date;
isThreadRoom: boolean; isThreadRoom: boolean;
id: string; id: string;
} }
@ -140,7 +142,7 @@ export interface IMessageInner
IMessageThread, IMessageThread,
IMessageAttachments, IMessageAttachments,
IMessageBroadcast { IMessageBroadcast {
type: string; type: TMessageType;
blocks: []; blocks: [];
} }

View File

@ -1,3 +1,4 @@
import { TMessageModel } from '../../definitions/IMessage';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DISCUSSION } from './constants'; import { DISCUSSION } from './constants';
@ -149,7 +150,7 @@ export const getInfoMessage = ({ type, role, msg, author }: TInfoMessage) => {
return ''; return '';
}; };
export const getMessageTranslation = (message: { translations: any }, autoTranslateLanguage: string) => { export const getMessageTranslation = (message: TMessageModel, autoTranslateLanguage: string) => {
if (!autoTranslateLanguage) { if (!autoTranslateLanguage) {
return null; return null;
} }

View File

@ -7,4 +7,4 @@ export interface IFrequentlyUsedEmoji {
count: number; count: number;
} }
export type TFrequentlyUsedEmoji = IFrequentlyUsedEmoji & Model; export type TFrequentlyUsedEmojiModel = IFrequentlyUsedEmoji & Model;

View File

@ -15,4 +15,4 @@ export interface ILoggedUser {
enableMessageParserEarlyAdoption?: boolean; enableMessageParserEarlyAdoption?: boolean;
} }
export type TLoggedUser = ILoggedUser & Model; export type TLoggedUserModel = ILoggedUser & Model;

View File

@ -7,4 +7,4 @@ export interface IServerHistory {
updatedAt: Date; updatedAt: Date;
} }
export type TServerHistory = IServerHistory & Model; export type TServerHistoryModel = IServerHistory & Model;

View File

@ -79,8 +79,6 @@ export interface ISubscription {
avatarETag?: string; avatarETag?: string;
teamId?: string; teamId?: string;
teamMain?: boolean; teamMain?: boolean;
search?: boolean;
username?: string;
// https://nozbe.github.io/WatermelonDB/Relation.html#relation-api // https://nozbe.github.io/WatermelonDB/Relation.html#relation-api
messages: Relation<TMessageModel>; messages: Relation<TMessageModel>;
threads: Relation<TThreadModel>; threads: Relation<TThreadModel>;

View File

@ -43,7 +43,7 @@ export interface IThread {
id: string; id: string;
msg?: string; msg?: string;
t?: SubscriptionType; t?: SubscriptionType;
rid?: string; rid: string;
_updatedAt?: Date; _updatedAt?: Date;
ts?: Date; ts?: Date;
u?: IUserMessage; u?: IUserMessage;
@ -61,10 +61,10 @@ export interface IThread {
reactions?: IReaction[]; reactions?: IReaction[];
role?: string; role?: string;
drid?: string; drid?: string;
dcount?: number; dcount?: number | string;
dlm?: number; dlm?: number;
tmid?: string; tmid?: string;
tcount: number | string; tcount?: number | string;
tlm?: string; tlm?: string;
replies?: string[]; replies?: string[];
mentions?: IUserMention[]; mentions?: IUserMention[];

View File

@ -3,12 +3,23 @@ import { StackNavigationProp } from '@react-navigation/stack';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
export * from './IAttachment'; export * from './IAttachment';
export * from './IMessage';
export * from './INotification'; export * from './INotification';
export * from './IRoom';
export * from './IServer';
export * from './ISubscription';
export * from './IPreferences'; export * from './IPreferences';
export * from './ISubscription';
export * from './IRoom';
export * from './IMessage';
export * from './IThread';
export * from './IThreadMessage';
export * from './ICustomEmoji';
export * from './IFrequentlyUsedEmoji';
export * from './IUpload';
export * from './ISettings';
export * from './IRole';
export * from './IPermission';
export * from './ISlashCommand';
export * from './IUser';
export * from './IServer';
export * from './ILoggedUser';
export * from './IServerHistory'; export * from './IServerHistory';
export interface IBaseScreen<T extends Record<string, object | undefined>, S extends string> { export interface IBaseScreen<T extends Record<string, object | undefined>, S extends string> {

View File

@ -775,6 +775,7 @@
"creating_discussion": "creating discussion", "creating_discussion": "creating discussion",
"Canned_Responses": "Canned Responses", "Canned_Responses": "Canned Responses",
"No_match_found": "No match found.", "No_match_found": "No match found.",
"No_discussions": "No discussions",
"Check_canned_responses": "Check on canned responses.", "Check_canned_responses": "Check on canned responses.",
"Searching": "Searching", "Searching": "Searching",
"Use": "Use", "Use": "Use",

View File

@ -25,6 +25,7 @@ import serversSchema from './schema/servers';
import appSchema from './schema/app'; import appSchema from './schema/app';
import migrations from './model/migrations'; import migrations from './model/migrations';
import serversMigrations from './model/servers/migrations'; import serversMigrations from './model/servers/migrations';
import { TAppDatabase, TServerDatabase } from './interfaces';
const appGroupPath = isIOS ? appGroup.path : ''; const appGroupPath = isIOS ? appGroup.path : '';
@ -32,9 +33,9 @@ if (__DEV__ && isIOS) {
console.log(appGroupPath); console.log(appGroupPath);
} }
const getDatabasePath = name => `${appGroupPath}${name}${isOfficial ? '' : '-experimental'}.db`; const getDatabasePath = (name: string) => `${appGroupPath}${name}${isOfficial ? '' : '-experimental'}.db`;
export const getDatabase = (database = '') => { export const getDatabase = (database = ''): Database => {
const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.'); const path = database.replace(/(^\w+:|^)\/\//, '').replace(/\//g, '.');
const dbName = getDatabasePath(path); const dbName = getDatabasePath(path);
@ -64,8 +65,14 @@ export const getDatabase = (database = '') => {
}); });
}; };
interface IDatabases {
shareDB?: TAppDatabase;
serversDB: TServerDatabase;
activeDB?: TAppDatabase;
}
class DB { class DB {
databases = { databases: IDatabases = {
serversDB: new Database({ serversDB: new Database({
adapter: new SQLiteAdapter({ adapter: new SQLiteAdapter({
dbName: getDatabasePath('default'), dbName: getDatabasePath('default'),
@ -73,11 +80,12 @@ class DB {
migrations: serversMigrations migrations: serversMigrations
}), }),
modelClasses: [Server, LoggedUser, ServersHistory] modelClasses: [Server, LoggedUser, ServersHistory]
}) }) as TServerDatabase
}; };
get active() { // Expected at least one database
return this.databases.shareDB || this.databases.activeDB; get active(): TAppDatabase {
return this.databases.shareDB || this.databases.activeDB!;
} }
get share() { get share() {
@ -116,11 +124,11 @@ class DB {
Setting, Setting,
User User
] ]
}); }) as TAppDatabase;
} }
setActiveDB(database) { setActiveDB(database: string) {
this.databases.activeDB = getDatabase(database); this.databases.activeDB = getDatabase(database) as TAppDatabase;
} }
} }

View File

@ -0,0 +1,72 @@
import { Database, Collection } from '@nozbe/watermelondb';
import * as models from './model';
import * as definitions from '../../definitions';
export type TAppDatabaseNames =
| typeof models.SUBSCRIPTIONS_TABLE
| typeof models.ROOMS_TABLE
| typeof models.MESSAGES_TABLE
| typeof models.THREADS_TABLE
| typeof models.THREAD_MESSAGES_TABLE
| typeof models.CUSTOM_EMOJIS_TABLE
| typeof models.FREQUENTLY_USED_EMOJIS_TABLE
| typeof models.UPLOADS_TABLE
| typeof models.SETTINGS_TABLE
| typeof models.ROLES_TABLE
| typeof models.PERMISSIONS_TABLE
| typeof models.SLASH_COMMANDS_TABLE
| typeof models.USERS_TABLE;
// Verify if T extends one type from TAppDatabaseNames, and if is truly,
// returns the specific model type.
// https://stackoverflow.com/a/54166010 TypeScript function return type based on input parameter
type ObjectType<T> = T extends typeof models.SUBSCRIPTIONS_TABLE
? definitions.TSubscriptionModel
: T extends typeof models.ROOMS_TABLE
? definitions.TRoomModel
: T extends typeof models.MESSAGES_TABLE
? definitions.TMessageModel
: T extends typeof models.THREADS_TABLE
? definitions.TThreadModel
: T extends typeof models.THREAD_MESSAGES_TABLE
? definitions.TThreadMessageModel
: T extends typeof models.CUSTOM_EMOJIS_TABLE
? definitions.TCustomEmojiModel
: T extends typeof models.FREQUENTLY_USED_EMOJIS_TABLE
? definitions.TFrequentlyUsedEmojiModel
: T extends typeof models.UPLOADS_TABLE
? definitions.TUploadModel
: T extends typeof models.SETTINGS_TABLE
? definitions.TSettingsModel
: T extends typeof models.ROLES_TABLE
? definitions.TRoleModel
: T extends typeof models.PERMISSIONS_TABLE
? definitions.TPermissionModel
: T extends typeof models.SLASH_COMMANDS_TABLE
? definitions.TSlashCommandModel
: T extends typeof models.USERS_TABLE
? definitions.TUserModel
: never;
export type TAppDatabase = {
get: <T extends TAppDatabaseNames>(db: T) => Collection<ObjectType<T>>;
} & Database;
// Migration to server database
export type TServerDatabaseNames =
| typeof models.SERVERS_TABLE
| typeof models.LOGGED_USERS_TABLE
| typeof models.SERVERS_HISTORY_TABLE;
type ObjectServerType<T> = T extends typeof models.SERVERS_TABLE
? definitions.TServerModel
: T extends typeof models.LOGGED_USERS_TABLE
? definitions.TLoggedUserModel
: T extends typeof models.SERVERS_HISTORY_TABLE
? definitions.TServerHistoryModel
: never;
export type TServerDatabase = {
get: <T extends TServerDatabaseNames>(db: T) => Collection<ObjectServerType<T>>;
} & Database;

View File

@ -3,8 +3,10 @@ import { date, field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils'; import { sanitizer } from '../utils';
export const CUSTOM_EMOJIS_TABLE = 'custom_emojis';
export default class CustomEmoji extends Model { export default class CustomEmoji extends Model {
static table = 'custom_emojis'; static table = CUSTOM_EMOJIS_TABLE;
@field('name') name; @field('name') name;

View File

@ -1,8 +1,9 @@
import { Model } from '@nozbe/watermelondb'; import { Model } from '@nozbe/watermelondb';
import { field } from '@nozbe/watermelondb/decorators'; import { field } from '@nozbe/watermelondb/decorators';
export const FREQUENTLY_USED_EMOJIS_TABLE = 'frequently_used_emojis';
export default class FrequentlyUsedEmoji extends Model { export default class FrequentlyUsedEmoji extends Model {
static table = 'frequently_used_emojis'; static table = FREQUENTLY_USED_EMOJIS_TABLE;
@field('content') content; @field('content') content;

View File

@ -3,10 +3,10 @@ import { date, field, json, relation } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils'; import { sanitizer } from '../utils';
export const TABLE_NAME = 'messages'; export const MESSAGES_TABLE = 'messages';
export default class Message extends Model { export default class Message extends Model {
static table = TABLE_NAME; static table = MESSAGES_TABLE;
static associations = { static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' } subscriptions: { type: 'belongs_to', key: 'rid' }

View File

@ -3,8 +3,10 @@ import { date, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils'; import { sanitizer } from '../utils';
export const PERMISSIONS_TABLE = 'permissions';
export default class Permission extends Model { export default class Permission extends Model {
static table = 'permissions'; static table = PERMISSIONS_TABLE;
@json('roles', sanitizer) roles; @json('roles', sanitizer) roles;

View File

@ -1,8 +1,10 @@
import { Model } from '@nozbe/watermelondb'; import { Model } from '@nozbe/watermelondb';
import { field } from '@nozbe/watermelondb/decorators'; import { field } from '@nozbe/watermelondb/decorators';
export const ROLES_TABLE = 'roles';
export default class Role extends Model { export default class Role extends Model {
static table = 'roles'; static table = ROLES_TABLE;
@field('description') description; @field('description') description;
} }

View File

@ -3,8 +3,10 @@ import { field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils'; import { sanitizer } from '../utils';
export const ROOMS_TABLE = 'rooms';
export default class Room extends Model { export default class Room extends Model {
static table = 'rooms'; static table = ROOMS_TABLE;
@json('custom_fields', sanitizer) customFields; @json('custom_fields', sanitizer) customFields;

View File

@ -1,8 +1,10 @@
import { Model } from '@nozbe/watermelondb'; import { Model } from '@nozbe/watermelondb';
import { date, field, readonly } from '@nozbe/watermelondb/decorators'; import { date, field, readonly } from '@nozbe/watermelondb/decorators';
export const SERVERS_HISTORY_TABLE = 'servers_history';
export default class ServersHistory extends Model { export default class ServersHistory extends Model {
static table = 'servers_history'; static table = SERVERS_HISTORY_TABLE;
@field('url') url; @field('url') url;

View File

@ -3,8 +3,10 @@ import { date, field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils'; import { sanitizer } from '../utils';
export const SETTINGS_TABLE = 'settings';
export default class Setting extends Model { export default class Setting extends Model {
static table = 'settings'; static table = SETTINGS_TABLE;
@field('value_as_string') valueAsString; @field('value_as_string') valueAsString;

View File

@ -1,8 +1,10 @@
import { Model } from '@nozbe/watermelondb'; import { Model } from '@nozbe/watermelondb';
import { field } from '@nozbe/watermelondb/decorators'; import { field } from '@nozbe/watermelondb/decorators';
export const SLASH_COMMANDS_TABLE = 'slash_commands';
export default class SlashCommand extends Model { export default class SlashCommand extends Model {
static table = 'slash_commands'; static table = SLASH_COMMANDS_TABLE;
@field('params') params; @field('params') params;

View File

@ -3,10 +3,10 @@ import { children, date, field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils'; import { sanitizer } from '../utils';
export const TABLE_NAME = 'subscriptions'; export const SUBSCRIPTIONS_TABLE = 'subscriptions';
export default class Subscription extends Model { export default class Subscription extends Model {
static table = TABLE_NAME; static table = SUBSCRIPTIONS_TABLE;
static associations = { static associations = {
messages: { type: 'has_many', foreignKey: 'rid' }, messages: { type: 'has_many', foreignKey: 'rid' },

View File

@ -3,10 +3,10 @@ import { date, field, json, relation } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils'; import { sanitizer } from '../utils';
export const TABLE_NAME = 'threads'; export const THREADS_TABLE = 'threads';
export default class Thread extends Model { export default class Thread extends Model {
static table = TABLE_NAME; static table = THREADS_TABLE;
static associations = { static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' } subscriptions: { type: 'belongs_to', key: 'rid' }

View File

@ -3,10 +3,10 @@ import { date, field, json, relation } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils'; import { sanitizer } from '../utils';
export const TABLE_NAME = 'thread_messages'; export const THREAD_MESSAGES_TABLE = 'thread_messages';
export default class ThreadMessage extends Model { export default class ThreadMessage extends Model {
static table = TABLE_NAME; static table = THREAD_MESSAGES_TABLE;
static associations = { static associations = {
subscriptions: { type: 'belongs_to', key: 'subscription_id' } subscriptions: { type: 'belongs_to', key: 'subscription_id' }

View File

@ -1,8 +1,10 @@
import { Model } from '@nozbe/watermelondb'; import { Model } from '@nozbe/watermelondb';
import { field, relation } from '@nozbe/watermelondb/decorators'; import { field, relation } from '@nozbe/watermelondb/decorators';
export const UPLOADS_TABLE = 'uploads';
export default class Upload extends Model { export default class Upload extends Model {
static table = 'uploads'; static table = UPLOADS_TABLE;
static associations = { static associations = {
subscriptions: { type: 'belongs_to', key: 'rid' } subscriptions: { type: 'belongs_to', key: 'rid' }

View File

@ -3,8 +3,10 @@ import { field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils'; import { sanitizer } from '../utils';
export const USERS_TABLE = 'users';
export default class User extends Model { export default class User extends Model {
static table = 'users'; static table = USERS_TABLE;
@field('_id') _id; @field('_id') _id;

View File

@ -0,0 +1,16 @@
export * from './CustomEmoji';
export * from './FrequentlyUsedEmoji';
export * from './Message';
export * from './Permission';
export * from './Role';
export * from './Room';
export * from './Setting';
export * from './SlashCommand';
export * from './Subscription';
export * from './Thread';
export * from './ThreadMessage';
export * from './Upload';
export * from './User';
export * from './ServersHistory';
export * from './servers/Server';
export * from './servers/User';

View File

@ -1,8 +1,10 @@
import { Model } from '@nozbe/watermelondb'; import { Model } from '@nozbe/watermelondb';
import { date, field } from '@nozbe/watermelondb/decorators'; import { date, field } from '@nozbe/watermelondb/decorators';
export const SERVERS_TABLE = 'servers';
export default class Server extends Model { export default class Server extends Model {
static table = 'servers'; static table = SERVERS_TABLE;
@field('name') name; @field('name') name;

View File

@ -3,8 +3,10 @@ import { field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../../utils'; import { sanitizer } from '../../utils';
export const LOGGED_USERS_TABLE = 'users';
export default class User extends Model { export default class User extends Model {
static table = 'users'; static table = LOGGED_USERS_TABLE;
@field('token') token; @field('token') token;

View File

@ -1,7 +1,7 @@
import database from '..'; import database from '..';
import { TABLE_NAME } from '../model/Message'; import { MESSAGES_TABLE } from '../model/Message';
const getCollection = db => db.get(TABLE_NAME); const getCollection = db => db.get(MESSAGES_TABLE);
export const getMessageById = async messageId => { export const getMessageById = async messageId => {
const db = database.active; const db = database.active;

View File

@ -1,7 +1,7 @@
import database from '..'; import database from '..';
import { TABLE_NAME } from '../model/Subscription'; import { SUBSCRIPTIONS_TABLE } from '../model/Subscription';
const getCollection = db => db.get(TABLE_NAME); const getCollection = db => db.get(SUBSCRIPTIONS_TABLE);
export const getSubscriptionByRoomId = async rid => { export const getSubscriptionByRoomId = async rid => {
const db = database.active; const db = database.active;

View File

@ -1,7 +1,7 @@
import database from '..'; import database from '..';
import { TABLE_NAME } from '../model/Thread'; import { THREADS_TABLE } from '../model/Thread';
const getCollection = db => db.get(TABLE_NAME); const getCollection = db => db.get(THREADS_TABLE);
export const getThreadById = async tmid => { export const getThreadById = async tmid => {
const db = database.active; const db = database.active;

View File

@ -1,7 +1,7 @@
import database from '..'; import database from '..';
import { TABLE_NAME } from '../model/ThreadMessage'; import { THREAD_MESSAGES_TABLE } from '../model/ThreadMessage';
const getCollection = db => db.get(TABLE_NAME); const getCollection = db => db.get(THREAD_MESSAGES_TABLE);
export const getThreadMessageById = async messageId => { export const getThreadMessageById = async messageId => {
const db = database.active; const db = database.active;

View File

@ -61,7 +61,7 @@ const PERMISSIONS = [
export async function setPermissions() { export async function setPermissions() {
const db = database.active; const db = database.active;
const permissionsCollection = db.collections.get('permissions'); const permissionsCollection = db.get('permissions');
const allPermissions = await permissionsCollection.query(Q.where('id', Q.oneOf(PERMISSIONS))).fetch(); const allPermissions = await permissionsCollection.query(Q.where('id', Q.oneOf(PERMISSIONS))).fetch();
const parsed = allPermissions.reduce((acc, item) => ({ ...acc, [item.id]: item.roles }), {}); const parsed = allPermissions.reduce((acc, item) => ({ ...acc, [item.id]: item.roles }), {});

View File

@ -8,7 +8,7 @@ import protectedFunction from './helpers/protectedFunction';
export async function setRoles() { export async function setRoles() {
const db = database.active; const db = database.active;
const rolesCollection = db.collections.get('roles'); const rolesCollection = db.get('roles');
const allRoles = await rolesCollection.query().fetch(); const allRoles = await rolesCollection.query().fetch();
const parsed = allRoles.reduce((acc, item) => ({ ...acc, [item.id]: item.description || item.id }), {}); const parsed = allRoles.reduce((acc, item) => ({ ...acc, [item.id]: item.description || item.id }), {});
reduxStore.dispatch(setRolesAction(parsed)); reduxStore.dispatch(setRolesAction(parsed));

View File

@ -811,6 +811,16 @@ const RocketChat = {
encrypted encrypted
}); });
}, },
getDiscussions({ roomId, offset, count, text }) {
const params = {
roomId,
offset,
count,
...(text && { text })
};
// RC 2.4.0
return this.sdk.get('chat.getDiscussions', params);
},
createTeam({ name, users, type, readOnly, broadcast, encrypted }) { createTeam({ name, users, type, readOnly, broadcast, encrypted }) {
const params = { const params = {
name, name,

View File

@ -66,6 +66,7 @@ import QueueListView from '../ee/omnichannel/views/QueueListView';
import AddChannelTeamView from '../views/AddChannelTeamView'; import AddChannelTeamView from '../views/AddChannelTeamView';
import AddExistingChannelView from '../views/AddExistingChannelView'; import AddExistingChannelView from '../views/AddExistingChannelView';
import SelectListView from '../views/SelectListView'; import SelectListView from '../views/SelectListView';
import DiscussionsView from '../views/DiscussionsView';
import { import {
AdminPanelStackParamList, AdminPanelStackParamList,
ChatsStackParamList, ChatsStackParamList,
@ -92,7 +93,8 @@ const ChatsStackNavigator = () => {
<ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} /> <ChatsStack.Screen name='SelectListView' component={SelectListView} options={SelectListView.navigationOptions} />
<ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} /> <ChatsStack.Screen name='RoomInfoView' component={RoomInfoView} options={RoomInfoView.navigationOptions} />
<ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} /> <ChatsStack.Screen name='RoomInfoEditView' component={RoomInfoEditView} options={RoomInfoEditView.navigationOptions} />
<ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} /> <ChatsStack.Screen name='RoomMembersView' component={RoomMembersView} options={RoomMembersView.navigationOptions} />
<ChatsStack.Screen name='DiscussionsView' component={DiscussionsView} />
<ChatsStack.Screen <ChatsStack.Screen
name='SearchMessagesView' name='SearchMessagesView'
component={SearchMessagesView} component={SearchMessagesView}

View File

@ -58,6 +58,7 @@ import QueueListView from '../../ee/omnichannel/views/QueueListView';
import AddChannelTeamView from '../../views/AddChannelTeamView'; import AddChannelTeamView from '../../views/AddChannelTeamView';
import AddExistingChannelView from '../../views/AddExistingChannelView'; import AddExistingChannelView from '../../views/AddExistingChannelView';
import SelectListView from '../../views/SelectListView'; import SelectListView from '../../views/SelectListView';
import DiscussionsView from '../../views/DiscussionsView';
import { ModalContainer } from './ModalContainer'; import { ModalContainer } from './ModalContainer';
import { import {
MasterDetailChatsStackParamList, MasterDetailChatsStackParamList,
@ -167,6 +168,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
<ModalStack.Screen name='LivechatEditView' component={LivechatEditView} options={LivechatEditView.navigationOptions} /> <ModalStack.Screen name='LivechatEditView' component={LivechatEditView} options={LivechatEditView.navigationOptions} />
<ModalStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} /> <ModalStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
<ModalStack.Screen name='ThreadMessagesView' component={ThreadMessagesView} /> <ModalStack.Screen name='ThreadMessagesView' component={ThreadMessagesView} />
<ModalStack.Screen name='DiscussionsView' component={DiscussionsView} />
<ModalStack.Screen name='TeamChannelsView' component={TeamChannelsView} options={TeamChannelsView.navigationOptions} /> <ModalStack.Screen name='TeamChannelsView' component={TeamChannelsView} options={TeamChannelsView.navigationOptions} />
<ModalStack.Screen name='MarkdownTableView' component={MarkdownTableView} options={MarkdownTableView.navigationOptions} /> <ModalStack.Screen name='MarkdownTableView' component={MarkdownTableView} options={MarkdownTableView.navigationOptions} />
<ModalStack.Screen <ModalStack.Screen

View File

@ -56,6 +56,10 @@ export type ModalStackParamList = {
rid: string; rid: string;
room: ISubscription; room: ISubscription;
}; };
DiscussionsView: {
rid: string;
t: SubscriptionType;
};
SearchMessagesView: { SearchMessagesView: {
rid: string; rid: string;
t: SubscriptionType; t: SubscriptionType;

View File

@ -54,6 +54,10 @@ export type ChatsStackParamList = {
rid: string; rid: string;
room: ISubscription; room: ISubscription;
}; };
DiscussionsView: {
rid: string;
t: SubscriptionType;
};
SearchMessagesView: { SearchMessagesView: {
rid: string; rid: string;
t: SubscriptionType; t: SubscriptionType;

View File

@ -31,9 +31,8 @@ const navigate = ({
}; };
interface IItem extends Partial<ISubscription> { interface IItem extends Partial<ISubscription> {
rid: string; search?: boolean; // comes from spotlight
name: string; username?: string;
t: SubscriptionType;
} }
export const goRoom = async ({ export const goRoom = async ({
@ -46,7 +45,7 @@ export const goRoom = async ({
navigationMethod?: any; navigationMethod?: any;
jumpToMessageId?: string; jumpToMessageId?: string;
}): Promise<void> => { }): Promise<void> => {
if (item.t === 'd' && item.search) { if (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;
@ -55,7 +54,7 @@ export const goRoom = async ({
return navigate({ return navigate({
item: { item: {
rid: result.room._id, rid: result.room._id,
name: username!, name: username,
t: SubscriptionType.DIRECT t: SubscriptionType.DIRECT
}, },
isMasterDetail, isMasterDetail,

View File

@ -22,11 +22,11 @@ import { goRoom } from '../utils/goRoom';
import { showErrorAlert } from '../utils/info'; import { showErrorAlert } from '../utils/info';
import debounce from '../utils/debounce'; import debounce from '../utils/debounce';
import { ChatsStackParamList } from '../stacks/types'; import { ChatsStackParamList } from '../stacks/types';
import { IRoom } from '../definitions/IRoom'; import { TSubscriptionModel, SubscriptionType } from '../definitions';
interface IAddExistingChannelViewState { interface IAddExistingChannelViewState {
search: Array<IRoom>; search: TSubscriptionModel[];
channels: Array<IRoom>; channels: TSubscriptionModel[];
selected: string[]; selected: string[];
loading: boolean; loading: boolean;
} }
@ -83,7 +83,7 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
try { try {
const { addTeamChannelPermission } = this.props; const { addTeamChannelPermission } = this.props;
const db = database.active; const db = database.active;
const channels = await db.collections const channels = await db
.get('subscriptions') .get('subscriptions')
.query( .query(
Q.where('team_id', ''), Q.where('team_id', ''),
@ -94,9 +94,9 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
) )
.fetch(); .fetch();
const asyncFilter = async (channelsArray: Array<IRoom>) => { const asyncFilter = async (channelsArray: TSubscriptionModel[]) => {
const results = await Promise.all( const results = await Promise.all(
channelsArray.map(async (channel: IRoom) => { channelsArray.map(async channel => {
if (channel.prid) { if (channel.prid) {
return false; return false;
} }
@ -173,11 +173,10 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
} }
}; };
// TODO: refactor with Room Model renderItem = ({ item }: { item: TSubscriptionModel }) => {
renderItem = ({ item }: { item: any }) => {
const isChecked = this.isChecked(item.rid); const isChecked = this.isChecked(item.rid);
// TODO: reuse logic inside RoomTypeIcon // TODO: reuse logic inside RoomTypeIcon
const icon = item.t === 'p' && !item.teamId ? 'channel-private' : 'channel-public'; const icon = item.t === SubscriptionType.DIRECT && !item?.teamId ? 'channel-private' : 'channel-public';
return ( return (
<List.Item <List.Item
title={RocketChat.getRoomTitle(item)} title={RocketChat.getRoomTitle(item)}

View File

@ -7,6 +7,7 @@ import RocketChat from '../../lib/rocketchat';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { MultiSelect } from '../../containers/UIKit/MultiSelect'; import { MultiSelect } from '../../containers/UIKit/MultiSelect';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { TSubscriptionModel } from '../../definitions/ISubscription';
import styles from './styles'; import styles from './styles';
import { ICreateDiscussionViewSelectChannel } from './interfaces'; import { ICreateDiscussionViewSelectChannel } from './interfaces';
@ -20,7 +21,7 @@ const SelectChannel = ({
serverVersion, serverVersion,
theme theme
}: ICreateDiscussionViewSelectChannel): JSX.Element => { }: ICreateDiscussionViewSelectChannel): JSX.Element => {
const [channels, setChannels] = useState([]); const [channels, setChannels] = useState<TSubscriptionModel[]>([]);
const getChannels = debounce(async (keyword = '') => { const getChannels = debounce(async (keyword = '') => {
try { try {

View File

@ -38,11 +38,11 @@ const SelectUsers = ({
const res = await RocketChat.search({ text: keyword, filterRooms: false }); const res = await RocketChat.search({ text: keyword, filterRooms: false });
let items = [ let items = [
...users.filter((u: IUser) => selected.includes(u.name)), ...users.filter((u: IUser) => selected.includes(u.name)),
...res.filter((r: IUser) => !users.find((u: IUser) => u.name === r.name)) ...res.filter(r => !users.find((u: IUser) => u.name === r.name))
]; ];
const records = await usersCollection.query(Q.where('username', Q.oneOf(items.map(u => u.name)))).fetch(); const records = await usersCollection.query(Q.where('username', Q.oneOf(items.map(u => u.name)))).fetch();
items = items.map(item => { items = items.map(item => {
const index = records.findIndex((r: IUser) => r.username === item.name); const index = records.findIndex(r => r.username === item.name);
if (index > -1) { if (index > -1) {
const record = records[index]; const record = records[index];
return { return {

View File

@ -0,0 +1,67 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { TThreadModel } from '../../definitions/IThread';
import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors';
import sharedStyles from '../Styles';
import { useTheme } from '../../theme';
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 8,
flexDirection: 'row',
alignItems: 'center'
},
detailsContainer: {
flex: 1,
flexDirection: 'row'
},
detailContainer: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 8
},
detailText: {
fontSize: 10,
marginLeft: 2,
...sharedStyles.textSemibold
}
});
interface IDiscussionDetails {
item: TThreadModel;
date: string;
}
const DiscussionDetails = ({ item, date }: IDiscussionDetails): JSX.Element => {
const { theme } = useTheme();
let { dcount } = item;
if (dcount && dcount >= 1000) {
dcount = '+999';
}
return (
<View style={[styles.container]}>
<View style={styles.detailsContainer}>
<View style={styles.detailContainer}>
<CustomIcon name={'discussions'} size={24} color={themes[theme!].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
{dcount}
</Text>
</View>
<View style={styles.detailContainer}>
<CustomIcon name={'clock'} size={24} color={themes[theme!].auxiliaryText} />
<Text style={[styles.detailText, { color: themes[theme!].auxiliaryText }]} numberOfLines={1}>
{date}
</Text>
</View>
</View>
</View>
);
};
export default DiscussionDetails;

View File

@ -0,0 +1,96 @@
/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types */
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { ScrollView } from 'react-native';
import { Provider } from 'react-redux';
import * as List from '../../containers/List';
import { themes } from '../../constants/colors';
import { ThemeContext } from '../../theme';
import { store } from '../../../storybook/stories';
import Item from './Item';
const author = {
_id: 'userid',
username: 'rocket.cat',
name: 'Rocket Cat'
};
const baseUrl = 'https://open.rocket.chat';
const date = new Date(2020, 10, 10, 10);
const longText =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const defaultItem = {
msg: 'Message content',
tcount: 1,
replies: [1],
ts: date,
tlm: date,
u: author,
attachments: []
};
const BaseItem = ({ item, ...props }) => (
<Item
baseUrl={baseUrl}
item={{
...defaultItem,
...item
}}
onPress={() => alert('pressed')}
{...props}
/>
);
const listDecorator = story => (
<ScrollView>
<List.Separator />
{story()}
<List.Separator />
</ScrollView>
);
const stories = storiesOf('Discussions.Item', module)
.addDecorator(listDecorator)
.addDecorator(story => <Provider store={store}>{story()}</Provider>);
stories.add('content', () => (
<>
<BaseItem />
<List.Separator />
<BaseItem
item={{
msg: longText
}}
/>
<List.Separator />
<BaseItem
item={{
dcount: 1000,
replies: [...new Array(1000)]
}}
/>
<List.Separator />
<BaseItem
item={{
msg: '',
attachments: [{ title: 'Attachment title' }]
}}
/>
<List.Separator />
<BaseItem useRealName />
</>
));
const ThemeStory = ({ theme }) => (
<ThemeContext.Provider value={{ theme }}>
<BaseItem badgeColor={themes[theme].mentionMeColor} />
</ThemeContext.Provider>
);
stories.add('themes', () => (
<>
<ThemeStory theme='light' />
<ThemeStory theme='dark' />
<ThemeStory theme='black' />
</>
));

View File

@ -0,0 +1,105 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import moment from 'moment';
import { useTheme } from '../../theme';
import Avatar from '../../containers/Avatar';
import sharedStyles from '../Styles';
import { themes } from '../../constants/colors';
import Markdown from '../../containers/markdown';
import { formatDateThreads, makeThreadName } from '../../utils/room';
import DiscussionDetails from './DiscussionDetails';
import { TThreadModel } from '../../definitions/IThread';
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
padding: 16
},
contentContainer: {
flexDirection: 'column',
flex: 1
},
titleContainer: {
flexDirection: 'row',
marginBottom: 2,
justifyContent: 'space-between'
},
title: {
flexShrink: 1,
fontSize: 18,
...sharedStyles.textMedium
},
time: {
fontSize: 14,
marginLeft: 4,
...sharedStyles.textRegular
},
avatar: {
marginRight: 8
},
messageContainer: {
flexDirection: 'row'
},
markdown: {
flex: 1
}
});
interface IItem {
item: TThreadModel;
baseUrl: string;
onPress: {
(...args: any[]): void;
stop(): void;
};
}
const Item = ({ item, baseUrl, onPress }: IItem): JSX.Element => {
const { theme } = useTheme();
const username = item?.u?.username;
let messageTime = '';
let messageDate = '';
if (item?.ts) {
messageTime = moment(item.ts).format('LT');
messageDate = formatDateThreads(item.ts);
}
return (
<Touchable
onPress={() => onPress(item)}
testID={`discussions-view-${item.msg}`}
style={{ backgroundColor: themes[theme].backgroundColor }}>
<View style={styles.container}>
<Avatar style={styles.avatar} text={item?.u?.username} size={36} borderRadius={4} theme={theme} />
<View style={styles.contentContainer}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: themes[theme].titleText }]} numberOfLines={1}>
{username}
</Text>
{messageTime ? <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{messageTime}</Text> : null}
</View>
<View style={styles.messageContainer}>
{username ? (
/* @ts-ignore */
<Markdown
msg={makeThreadName(item)}
baseUrl={baseUrl}
username={username}
theme={theme}
numberOfLines={2}
style={[styles.markdown]}
preview
/>
) : null}
</View>
{messageDate ? <DiscussionDetails item={item} date={messageDate} /> : null}
</View>
</View>
</Touchable>
);
};
export default Item;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,208 @@
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { useSelector } from 'react-redux';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/core';
import { IApplicationState } from '../../definitions';
import { ChatsStackParamList } from '../../stacks/types';
import ActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n';
import StatusBar from '../../containers/StatusBar';
import log from '../../utils/log';
import debounce from '../../utils/debounce';
import { themes } from '../../constants/colors';
import SafeAreaView from '../../containers/SafeAreaView';
import * as HeaderButton from '../../containers/HeaderButton';
import * as List from '../../containers/List';
import BackgroundContainer from '../../containers/BackgroundContainer';
import { isIOS } from '../../utils/deviceInfo';
import { getHeaderTitlePosition } from '../../containers/Header';
import { useTheme } from '../../theme';
import RocketChat from '../../lib/rocketchat';
import SearchHeader from '../../containers/SearchHeader';
import { TThreadModel } from '../../definitions/IThread';
import Item from './Item';
const API_FETCH_COUNT = 50;
const styles = StyleSheet.create({
contentContainer: {
marginBottom: 30
}
});
interface IDiscussionsViewProps {
navigation: StackNavigationProp<ChatsStackParamList, 'DiscussionsView'>;
route: RouteProp<ChatsStackParamList, 'DiscussionsView'>;
item: TThreadModel;
}
const DiscussionsView = ({ navigation, route }: IDiscussionsViewProps): JSX.Element => {
const rid = route.params?.rid;
const t = route.params?.t;
const baseUrl = useSelector((state: IApplicationState) => state.server?.server);
const isMasterDetail = useSelector((state: IApplicationState) => state.app?.isMasterDetail);
const [loading, setLoading] = useState(false);
const [discussions, setDiscussions] = useState([]);
const [search, setSearch] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const [total, setTotal] = useState(0);
const [searchTotal, setSearchTotal] = useState(0);
const { theme } = useTheme();
const insets = useSafeAreaInsets();
const load = async (text = '') => {
if (loading) {
return;
}
setLoading(true);
try {
const result = await RocketChat.getDiscussions({
roomId: rid,
offset: isSearching ? search.length : discussions.length,
count: API_FETCH_COUNT,
text
});
if (result.success) {
if (isSearching) {
setSearch(result.messages);
setSearchTotal(result.total);
} else {
setDiscussions(result.messages);
setTotal(result.total);
}
}
setLoading(false);
} catch (e) {
log(e);
setLoading(false);
}
};
const onSearchChangeText = debounce(async (text: string) => {
setIsSearching(true);
await load(text);
}, 300);
const onCancelSearchPress = () => {
setIsSearching(false);
setSearch([]);
setSearchTotal(0);
};
const onSearchPress = () => {
setIsSearching(true);
};
const setHeader = () => {
let options: Partial<StackNavigationOptions>;
if (isSearching) {
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 });
options = {
headerTitleAlign: 'left',
headerLeft: () => (
<HeaderButton.Container left>
<HeaderButton.Item iconName='close' onPress={onCancelSearchPress} />
</HeaderButton.Container>
),
headerTitle: () => (
<SearchHeader onSearchChangeText={onSearchChangeText} testID='discussion-messages-view-search-header' />
),
headerTitleContainerStyle: {
left: headerTitlePosition.left,
right: headerTitlePosition.right
},
headerRight: () => null
};
return options;
}
options = {
headerLeft: () => (
<HeaderBackButton labelVisible={false} onPress={() => navigation.pop()} tintColor={themes[theme].headerTintColor} />
),
headerTitleAlign: 'center',
headerTitle: I18n.t('Discussions'),
headerTitleContainerStyle: {
left: null,
right: null
},
headerRight: () => (
<HeaderButton.Container>
<HeaderButton.Item iconName='search' onPress={onSearchPress} />
</HeaderButton.Container>
)
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
}
return options;
};
useEffect(() => {
load();
}, []);
useLayoutEffect(() => {
const options = setHeader();
navigation.setOptions(options);
}, [navigation, isSearching]);
const onDiscussionPress = debounce(
(item: TThreadModel) => {
if (item.drid && item.t) {
navigation.push('RoomView', {
rid: item.drid,
prid: item.rid,
name: item.msg,
t
});
}
},
1000,
true
);
const renderItem = ({ item }: { item: TThreadModel }) => (
<Item
{...{
item,
baseUrl
}}
onPress={onDiscussionPress}
/>
);
if (!discussions?.length) {
return <BackgroundContainer loading={loading} text={I18n.t('No_discussions')} />;
}
return (
<SafeAreaView testID='discussions-view'>
<StatusBar />
<FlatList
data={isSearching ? search : discussions}
renderItem={renderItem}
keyExtractor={(item: any) => item.msg}
style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={styles.contentContainer}
onEndReachedThreshold={0.5}
removeClippedSubviews={isIOS}
onEndReached={() => (isSearching ? searchTotal : total) > API_FETCH_COUNT ?? load()}
ItemSeparatorComponent={List.Separator}
ListFooterComponent={loading ? <ActivityIndicator theme={theme} /> : null}
scrollIndicatorInsets={{ right: 1 }}
/>
</SafeAreaView>
);
};
export default DiscussionsView;

View File

@ -24,6 +24,7 @@ import { createChannelRequest } from '../actions/createChannel';
import { goRoom } from '../utils/goRoom'; import { goRoom } from '../utils/goRoom';
import SafeAreaView from '../containers/SafeAreaView'; import SafeAreaView from '../containers/SafeAreaView';
import { compareServerVersion, methods } from '../lib/utils'; import { compareServerVersion, methods } from '../lib/utils';
import { TSubscriptionModel } from '../definitions';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
const QUERY_SIZE = 50; const QUERY_SIZE = 50;
@ -68,9 +69,8 @@ interface ISearch {
} }
interface INewMessageViewState { interface INewMessageViewState {
search: ISearch[]; search: (ISearch | TSubscriptionModel)[];
// TODO: Refactor when migrate room chats: TSubscriptionModel[];
chats: any[];
permissions: boolean[]; permissions: boolean[];
} }
@ -108,7 +108,7 @@ class NewMessageView extends React.Component<INewMessageViewProps, INewMessageVi
init = async () => { init = async () => {
try { try {
const db = database.active; const db = database.active;
const chats = await db.collections const chats = await db
.get('subscriptions') .get('subscriptions')
.query(Q.where('t', 'd'), Q.experimentalTake(QUERY_SIZE), Q.experimentalSortBy('room_updated_at', Q.desc)) .query(Q.where('t', 'd'), Q.experimentalTake(QUERY_SIZE), Q.experimentalSortBy('room_updated_at', Q.desc))
.fetch(); .fetch();
@ -153,7 +153,7 @@ class NewMessageView extends React.Component<INewMessageViewProps, INewMessageVi
}; };
search = async (text: string) => { search = async (text: string) => {
const result = await RocketChat.search({ text, filterRooms: false }); const result: ISearch[] | TSubscriptionModel[] = await RocketChat.search({ text, filterRooms: false });
this.setState({ this.setState({
search: result search: result
}); });
@ -280,8 +280,7 @@ class NewMessageView extends React.Component<INewMessageViewProps, INewMessageVi
); );
}; };
// TODO: Refactor when migrate room renderItem = ({ item, index }: { item: ISearch | TSubscriptionModel; index: number }) => {
renderItem = ({ item, index }: { item: ISearch | any; index: number }) => {
const { search, chats } = this.state; const { search, chats } = this.state;
const { theme } = this.props; const { theme } = this.props;
@ -295,10 +294,14 @@ class NewMessageView extends React.Component<INewMessageViewProps, INewMessageVi
if (search.length === 0 && index === chats.length - 1) { if (search.length === 0 && index === chats.length - 1) {
style = { ...style, ...sharedStyles.separatorBottom }; style = { ...style, ...sharedStyles.separatorBottom };
} }
const itemSearch = item as ISearch;
const itemModel = item as TSubscriptionModel;
return ( return (
<UserItem <UserItem
name={item.search ? item.name : item.fname} name={itemSearch.search ? itemSearch.name : itemModel.fname || ''}
username={item.search ? item.username : item.name} username={itemSearch.search ? itemSearch.username : itemModel.name}
onPress={() => this.goRoom(item)} onPress={() => this.goRoom(item)}
testID={`new-message-view-item-${item.name}`} testID={`new-message-view-item-${item.name}`}
style={style} style={style}

View File

@ -6,7 +6,7 @@ import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import sharedStyles from '../../Styles'; import sharedStyles from '../../Styles';
import Touch from '../../../utils/touch'; import Touch from '../../../utils/touch';
import { TServerHistory } from '../../../definitions/IServerHistory'; import { TServerHistoryModel } from '../../../definitions/IServerHistory';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -28,10 +28,10 @@ const styles = StyleSheet.create({
}); });
interface IItem { interface IItem {
item: TServerHistory; item: TServerHistoryModel;
theme: string; theme: string;
onPress(url: string): void; onPress(url: string): void;
onDelete(item: TServerHistory): void; onDelete(item: TServerHistoryModel): void;
} }
const Item = ({ item, theme, onPress, onDelete }: IItem): JSX.Element => ( const Item = ({ item, theme, onPress, onDelete }: IItem): JSX.Element => (

View File

@ -5,7 +5,7 @@ import TextInput from '../../../containers/TextInput';
import * as List from '../../../containers/List'; import * as List from '../../../containers/List';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import { TServerHistory } from '../../../definitions/IServerHistory'; import { TServerHistoryModel } from '../../../definitions/IServerHistory';
import Item from './Item'; import Item from './Item';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -33,8 +33,8 @@ interface IServerInput extends TextInputProps {
theme: string; theme: string;
serversHistory: any[]; serversHistory: any[];
onSubmit(): void; onSubmit(): void;
onDelete(item: TServerHistory): void; onDelete(item: TServerHistoryModel): void;
onPressServerHistory(serverHistory: TServerHistory): void; onPressServerHistory(serverHistory: TServerHistoryModel): void;
} }
const ServerInput = ({ const ServerInput = ({

View File

@ -14,7 +14,7 @@ import Button from '../../containers/Button';
import FormContainer, { FormContainerInner } from '../../containers/FormContainer'; import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
import * as HeaderButton from '../../containers/HeaderButton'; import * as HeaderButton from '../../containers/HeaderButton';
import OrSeparator from '../../containers/OrSeparator'; import OrSeparator from '../../containers/OrSeparator';
import { IBaseScreen, TServerHistory } from '../../definitions'; import { IBaseScreen, TServerHistoryModel } from '../../definitions';
import { withDimensions } from '../../dimensions'; import { withDimensions } from '../../dimensions';
import I18n from '../../i18n'; import I18n from '../../i18n';
import database from '../../lib/database'; import database from '../../lib/database';
@ -77,7 +77,7 @@ interface INewServerViewState {
text: string; text: string;
connectingOpen: boolean; connectingOpen: boolean;
certificate: any; certificate: any;
serversHistory: TServerHistory[]; serversHistory: TServerHistoryModel[];
} }
interface ISubmitParams { interface ISubmitParams {
@ -153,7 +153,7 @@ class NewServerView extends React.Component<INewServerViewProps, INewServerViewS
const likeString = sanitizeLikeString(text); const likeString = sanitizeLikeString(text);
whereClause = [...whereClause, Q.where('url', Q.like(`%${likeString}%`))]; whereClause = [...whereClause, Q.where('url', Q.like(`%${likeString}%`))];
} }
const serversHistory = (await serversHistoryCollection.query(...whereClause).fetch()) as TServerHistory[]; const serversHistory = await serversHistoryCollection.query(...whereClause).fetch();
this.setState({ serversHistory }); this.setState({ serversHistory });
} catch { } catch {
// Do nothing // Do nothing
@ -177,7 +177,7 @@ class NewServerView extends React.Component<INewServerViewProps, INewServerViewS
dispatch(serverRequest(server)); dispatch(serverRequest(server));
}; };
onPressServerHistory = (serverHistory: TServerHistory) => { onPressServerHistory = (serverHistory: TServerHistoryModel) => {
this.setState({ text: serverHistory.url }, () => this.submit({ fromServerHistory: true, username: serverHistory?.username })); this.setState({ text: serverHistory.url }, () => this.submit({ fromServerHistory: true, username: serverHistory?.username }));
}; };
@ -269,14 +269,14 @@ class NewServerView extends React.Component<INewServerViewProps, INewServerViewS
}); });
}; };
deleteServerHistory = async (item: TServerHistory) => { deleteServerHistory = async (item: TServerHistoryModel) => {
const db = database.servers; const db = database.servers;
try { try {
await db.write(async () => { await db.write(async () => {
await item.destroyPermanently(); await item.destroyPermanently();
}); });
this.setState((prevstate: INewServerViewState) => ({ this.setState((prevstate: INewServerViewState) => ({
serversHistory: prevstate.serversHistory.filter((server: TServerHistory) => server.id !== item.id) serversHistory: prevstate.serversHistory.filter(server => server.id !== item.id)
})); }));
} catch { } catch {
// Nothing // Nothing

View File

@ -80,7 +80,7 @@ class NotificationPreferencesView extends React.Component<INotificationPreferenc
const db = database.active; const db = database.active;
try { try {
await db.action(async () => { await db.write(async () => {
await room.update( await room.update(
protectedFunction((r: any) => { protectedFunction((r: any) => {
r[key] = value; r[key] = value;
@ -97,7 +97,7 @@ class NotificationPreferencesView extends React.Component<INotificationPreferenc
// do nothing // do nothing
} }
await db.action(async () => { await db.write(async () => {
await room.update( await room.update(
protectedFunction((r: any) => { protectedFunction((r: any) => {
r[key] = room[key]; r[key] = room[key];

View File

@ -643,7 +643,7 @@ class RoomActionsView extends React.Component {
const { addTeamChannelPermission, createTeamPermission } = this.props; const { addTeamChannelPermission, createTeamPermission } = this.props;
const QUERY_SIZE = 50; const QUERY_SIZE = 50;
const db = database.active; const db = database.active;
const teams = await db.collections const teams = await db
.get('subscriptions') .get('subscriptions')
.query( .query(
Q.where('team_main', true), Q.where('team_main', true),
@ -941,7 +941,7 @@ class RoomActionsView extends React.Component {
canReturnQueue, canReturnQueue,
canViewCannedResponse canViewCannedResponse
} = this.state; } = this.state;
const { rid, t } = room; const { rid, t, prid } = room;
const isGroupChat = RocketChat.isGroupChat(room); const isGroupChat = RocketChat.isGroupChat(room);
return ( return (
@ -1009,6 +1009,27 @@ class RoomActionsView extends React.Component {
</> </>
) : null} ) : null}
{['c', 'p', 'd'].includes(t) && !prid ? (
<>
<List.Item
title='Discussions'
onPress={() =>
this.onPressTouchable({
route: 'DiscussionsView',
params: {
rid,
t
}
})
}
testID='room-actions-discussions'
left={() => <List.Icon name='discussions' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['c', 'p', 'd'].includes(t) ? ( {['c', 'p', 'd'].includes(t) ? (
<> <>
<List.Item <List.Item

View File

@ -144,11 +144,11 @@ class ListContainer extends React.Component {
if (tmid) { if (tmid) {
try { try {
this.thread = await db.collections.get('threads').find(tmid); this.thread = await db.get('threads').find(tmid);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
this.messagesObservable = db.collections this.messagesObservable = db
.get('thread_messages') .get('thread_messages')
.query(Q.where('rid', tmid), Q.experimentalSortBy('ts', Q.desc), Q.experimentalSkip(0), Q.experimentalTake(this.count)) .query(Q.where('rid', tmid), Q.experimentalSortBy('ts', Q.desc), Q.experimentalSkip(0), Q.experimentalTake(this.count))
.observe(); .observe();
@ -162,7 +162,7 @@ class ListContainer extends React.Component {
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))));
} }
this.messagesObservable = db.collections this.messagesObservable = db
.get('messages') .get('messages')
.query(...whereClause) .query(...whereClause)
.observe(); .observe();

View File

@ -91,7 +91,7 @@ class UploadProgress extends Component {
} }
const db = database.active; const db = database.active;
this.uploadsObservable = db.collections.get('uploads').query(Q.where('rid', rid)).observeWithColumns(['progress', 'error']); this.uploadsObservable = db.get('uploads').query(Q.where('rid', rid)).observeWithColumns(['progress', 'error']);
this.uploadsSubscription = this.uploadsObservable.subscribe(uploads => { this.uploadsSubscription = this.uploadsObservable.subscribe(uploads => {
if (this.mounted) { if (this.mounted) {

View File

@ -459,7 +459,7 @@ class RoomView extends React.Component {
} }
}); });
} else { } else {
navigation.navigate('RoomActionsView', { navigation.push('RoomActionsView', {
rid: this.rid, rid: this.rid,
t: this.t, t: this.t,
room, room,
@ -690,7 +690,7 @@ class RoomView extends React.Component {
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
updateUnreadCount = async () => { updateUnreadCount = async () => {
const db = database.active; const db = database.active;
const observable = await db.collections const observable = await db
.get('subscriptions') .get('subscriptions')
.query(Q.where('archived', false), Q.where('open', true), Q.where('rid', Q.notEq(this.rid))) .query(Q.where('archived', false), Q.where('open', true), Q.where('rid', Q.notEq(this.rid)))
.observeWithColumns(['unread']); .observeWithColumns(['unread']);

View File

@ -47,7 +47,7 @@ class ServerDropdown extends Component {
async componentDidMount() { async componentDidMount() {
const serversDB = database.servers; const serversDB = database.servers;
const observable = await serversDB.collections.get('servers').query().observeWithColumns(['name']); const observable = await serversDB.get('servers').query().observeWithColumns(['name']);
this.subscription = observable.subscribe(data => { this.subscription = observable.subscribe(data => {
this.setState({ servers: data }); this.setState({ servers: data });

View File

@ -450,7 +450,7 @@ class RoomsListView extends React.Component {
// When we're grouping by something // When we're grouping by something
if (this.isGrouping) { if (this.isGrouping) {
observable = await db.collections observable = await db
.get('subscriptions') .get('subscriptions')
.query(...defaultWhereClause) .query(...defaultWhereClause)
.observeWithColumns(['alert']); .observeWithColumns(['alert']);
@ -458,7 +458,7 @@ class RoomsListView extends React.Component {
// When we're NOT grouping // When we're NOT grouping
} else { } else {
this.count += QUERY_SIZE; this.count += QUERY_SIZE;
observable = await db.collections observable = await db
.get('subscriptions') .get('subscriptions')
.query(...defaultWhereClause, Q.experimentalSkip(0), Q.experimentalTake(this.count)) .query(...defaultWhereClause, Q.experimentalSkip(0), Q.experimentalTake(this.count))
.observe(); .observe();

View File

@ -92,7 +92,7 @@ class ScreenLockConfigView extends React.Component<IScreenLockConfigViewProps, I
const serversDB = database.servers; const serversDB = database.servers;
const serversCollection = serversDB.get('servers'); const serversCollection = serversDB.get('servers');
try { try {
this.serverRecord = (await serversCollection.find(server)) as TServerModel; this.serverRecord = await serversCollection.find(server);
this.setState({ this.setState({
autoLock: this.serverRecord?.autoLock, autoLock: this.serverRecord?.autoLock,
autoLockTime: this.serverRecord?.autoLockTime === null ? DEFAULT_AUTO_LOCK : this.serverRecord?.autoLockTime, autoLockTime: this.serverRecord?.autoLockTime === null ? DEFAULT_AUTO_LOCK : this.serverRecord?.autoLockTime,
@ -115,7 +115,7 @@ class ScreenLockConfigView extends React.Component<IScreenLockConfigViewProps, I
*/ */
observe = () => { observe = () => {
this.observable = this.serverRecord?.observe()?.subscribe(({ biometry }) => { this.observable = this.serverRecord?.observe()?.subscribe(({ biometry }) => {
this.setState({ biometry }); this.setState({ biometry: !!biometry });
}); });
}; };

View File

@ -109,10 +109,7 @@ class SelectedUsersView extends React.Component<ISelectedUsersViewProps, ISelect
init = async () => { init = async () => {
try { try {
const db = database.active; const db = database.active;
const observable = await db.collections const observable = await db.get('subscriptions').query(Q.where('t', 'd')).observeWithColumns(['room_updated_at']);
.get('subscriptions')
.query(Q.where('t', 'd'))
.observeWithColumns(['room_updated_at']);
// TODO: Refactor when migrate room // TODO: Refactor when migrate room
this.querySubscription = observable.subscribe((data: any) => { this.querySubscription = observable.subscribe((data: any) => {
@ -129,7 +126,9 @@ class SelectedUsersView extends React.Component<ISelectedUsersViewProps, ISelect
} }
search = async (text: string) => { search = async (text: string) => {
const result = await RocketChat.search({ text, filterRooms: false }); // TODO: When migrate rocketchat.js pass the param IUser to there and the return should be
// IUser | TSubscriptionModel, this because we do a local search too
const result = (await RocketChat.search({ text, filterRooms: false })) as ISelectedUser[];
this.setState({ this.setState({
search: result search: result
}); });

View File

@ -218,7 +218,9 @@ class TeamChannelsView extends React.Component<ITeamChannelsViewProps, ITeamChan
<HeaderButton.Item iconName='close' onPress={this.onCancelSearchPress} /> <HeaderButton.Item iconName='close' onPress={this.onCancelSearchPress} />
</HeaderButton.Container> </HeaderButton.Container>
), ),
headerTitle: () => <SearchHeader onSearchChangeText={this.onSearchChangeText} />, headerTitle: () => (
<SearchHeader onSearchChangeText={this.onSearchChangeText} testID='team-channels-view-search-header' />
),
headerTitleContainerStyle: { headerTitleContainerStyle: {
left: headerTitlePosition.left, left: headerTitlePosition.left,
right: headerTitlePosition.right right: headerTitlePosition.right

View File

@ -2,12 +2,12 @@
import React from 'react'; import React from 'react';
import { storiesOf } from '@storybook/react-native'; import { storiesOf } from '@storybook/react-native';
import { ScrollView } from 'react-native'; import { ScrollView } from 'react-native';
import { combineReducers, createStore } from 'redux';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import * as List from '../../containers/List'; import * as List from '../../containers/List';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { ThemeContext } from '../../theme'; import { ThemeContext } from '../../theme';
import { store } from '../../../storybook/stories';
import Item from './Item'; import Item from './Item';
const author = { const author = {
@ -49,28 +49,6 @@ const listDecorator = story => (
</ScrollView> </ScrollView>
); );
const reducers = combineReducers({
login: () => ({
user: {
id: 'abc',
username: 'rocket.cat',
name: 'Rocket Cat'
}
}),
server: () => ({
server: 'https://open.rocket.chat',
version: '3.7.0'
}),
share: () => ({
server: 'https://open.rocket.chat',
version: '3.7.0'
}),
settings: () => ({
blockUnauthenticatedAccess: false
})
});
const store = createStore(reducers);
const stories = storiesOf('Thread Messages.Item', module) const stories = storiesOf('Thread Messages.Item', module)
.addDecorator(listDecorator) .addDecorator(listDecorator)
.addDecorator(story => <Provider store={store}>{story()}</Provider>); .addDecorator(story => <Provider store={store}>{story()}</Provider>);

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { withTheme } from '../../theme'; import { useTheme } from '../../theme';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -59,7 +59,6 @@ const styles = StyleSheet.create({
interface IItem { interface IItem {
item: TThreadModel; item: TThreadModel;
baseUrl: string; baseUrl: string;
theme?: string;
useRealName: boolean; useRealName: boolean;
user: any; user: any;
badgeColor?: string; badgeColor?: string;
@ -67,7 +66,8 @@ interface IItem {
toggleFollowThread: (isFollowing: boolean, id: string) => void; toggleFollowThread: (isFollowing: boolean, id: string) => void;
} }
const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, toggleFollowThread }: IItem) => { const Item = ({ item, baseUrl, useRealName, user, badgeColor, onPress, toggleFollowThread }: IItem) => {
const { theme } = useTheme();
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) {
@ -89,16 +89,18 @@ const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, to
<Text style={[styles.time, { color: themes[theme!].auxiliaryText }]}>{time}</Text> <Text style={[styles.time, { color: themes[theme!].auxiliaryText }]}>{time}</Text>
</View> </View>
<View style={styles.messageContainer}> <View style={styles.messageContainer}>
{makeThreadName(item) && username ? (
/* @ts-ignore */
<Markdown <Markdown
// @ts-ignore
msg={makeThreadName(item)} msg={makeThreadName(item)}
baseUrl={baseUrl} baseUrl={baseUrl}
username={username!} username={username}
theme={theme!} theme={theme}
numberOfLines={2} numberOfLines={2}
style={[styles.markdown]} style={[styles.markdown]}
preview preview
/> />
) : null}
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null} {badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null}
</View> </View>
<ThreadDetails item={item} user={user} toggleFollowThread={toggleFollowThread} style={styles.threadDetails} /> <ThreadDetails item={item} user={user} toggleFollowThread={toggleFollowThread} style={styles.threadDetails} />
@ -108,4 +110,4 @@ const Item = ({ item, baseUrl, theme, useRealName, user, badgeColor, onPress, to
); );
}; };
export default withTheme(Item); export default Item;

View File

@ -7,7 +7,6 @@ import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native'; import { RouteProp } from '@react-navigation/native';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import Model from '@nozbe/watermelondb/Model';
import Database from '@nozbe/watermelondb/Database'; import Database from '@nozbe/watermelondb/Database';
import ActivityIndicator from '../../containers/ActivityIndicator'; import ActivityIndicator from '../../containers/ActivityIndicator';
@ -86,7 +85,7 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
private messagesSubscription?: Subscription; private messagesSubscription?: Subscription;
private messagesObservable!: Observable<Model>; private messagesObservable?: Observable<TThreadModel[]>;
constructor(props: IThreadMessagesViewProps) { constructor(props: IThreadMessagesViewProps) {
super(props); super(props);
@ -144,7 +143,9 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
<HeaderButton.Item iconName='close' onPress={this.onCancelSearchPress} /> <HeaderButton.Item iconName='close' onPress={this.onCancelSearchPress} />
</HeaderButton.Container> </HeaderButton.Container>
), ),
headerTitle: () => <SearchHeader onSearchChangeText={this.onSearchChangeText} />, headerTitle: () => (
<SearchHeader onSearchChangeText={this.onSearchChangeText} testID='thread-messages-view-search-header' />
),
headerTitleContainerStyle: { headerTitleContainerStyle: {
left: headerTitlePosition.left, left: headerTitlePosition.left,
right: headerTitlePosition.right right: headerTitlePosition.right
@ -188,7 +189,7 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
const db = database.active; const db = database.active;
// subscription query // subscription query
const subscription = (await db.collections.get('subscriptions').find(this.rid)) as TSubscriptionModel; const subscription = await db.get('subscriptions').find(this.rid);
const observable = subscription.observe(); const observable = subscription.observe();
this.subSubscription = observable.subscribe(data => { this.subSubscription = observable.subscribe(data => {
this.setState({ subscription: data }); this.setState({ subscription: data });
@ -214,15 +215,15 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
whereClause.push(Q.where('msg', Q.like(`%${sanitizeLikeString(searchText.trim())}%`))); whereClause.push(Q.where('msg', Q.like(`%${sanitizeLikeString(searchText.trim())}%`)));
} }
this.messagesObservable = db.collections this.messagesObservable = db
.get('threads') .get('threads')
.query(...whereClause) .query(...whereClause)
.observeWithColumns(['updated_at']); .observeWithColumns(['updated_at']);
// TODO: Refactor when migrate messages // TODO: Refactor when migrate messages
this.messagesSubscription = this.messagesObservable.subscribe((messages: any) => { this.messagesSubscription = this.messagesObservable.subscribe(messages => {
const { currentFilter } = this.state; const { currentFilter } = this.state;
const displayingThreads = this.getFilteredThreads(messages, subscription!, currentFilter); const displayingThreads = this.getFilteredThreads(messages, subscription, currentFilter);
if (this.mounted) { if (this.mounted) {
this.setState({ messages, displayingThreads }); this.setState({ messages, displayingThreads });
} else { } else {
@ -419,14 +420,14 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
}; };
// helper to query threads // helper to query threads
getFilteredThreads = (messages: any, subscription: TSubscriptionModel, currentFilter?: Filter): TThreadModel[] => { getFilteredThreads = (messages: TThreadModel[], subscription?: TSubscriptionModel, currentFilter?: Filter): TThreadModel[] => {
// const { currentFilter } = this.state; // const { currentFilter } = this.state;
const { user } = this.props; const { user } = this.props;
if (currentFilter === Filter.Following) { if (currentFilter === Filter.Following) {
return messages?.filter((item: { replies: any[] }) => item?.replies?.find(u => u === user.id)); return messages?.filter(item => item?.replies?.find(u => u === user.id));
} }
if (currentFilter === Filter.Unread) { if (currentFilter === Filter.Unread) {
return messages?.filter((item: { id: string }) => subscription?.tunread?.includes(item?.id)); return messages?.filter(item => subscription?.tunread?.includes(item?.id));
} }
return messages; return messages;
}; };

View File

@ -107,7 +107,7 @@ describe('Discussion', () => {
}); });
describe('Check RoomActionsView render', () => { describe('Check RoomActionsView render', () => {
it('should navigete to RoomActionsView', async () => { it('should navigate to RoomActionsView', async () => {
await waitFor(element(by.id('room-header'))) await waitFor(element(by.id('room-header')))
.toBeVisible() .toBeVisible()
.withTimeout(5000); .withTimeout(5000);
@ -173,4 +173,51 @@ describe('Discussion', () => {
await expect(element(by.id('room-info-view-edit-button'))).toBeVisible(); await expect(element(by.id('room-info-view-edit-button'))).toBeVisible();
}); });
}); });
describe('Open Discussion from DiscussionsView', () => {
const discussionName = `${data.random}message`;
it('should go back to main room', async () => {
await tapBack();
await waitFor(element(by.id('room-actions-view')))
.toBeVisible()
.withTimeout(5000);
await tapBack();
await waitFor(element(by.id(`room-view-title-${discussionName}`)))
.toExist()
.withTimeout(5000);
await tapBack();
await navigateToRoom();
});
it('should navigate to DiscussionsView', async () => {
await waitFor(element(by.id(`room-view-title-${channel}`)))
.toExist()
.withTimeout(5000);
await waitFor(element(by.id('room-header')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('room-header')).tap();
await waitFor(element(by.id('room-actions-discussions')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('room-actions-discussions')).tap();
await waitFor(element(by.id('discussions-view')))
.toBeVisible()
.withTimeout(5000);
});
it('should navigate to discussion', async () => {
const discussionName = `${data.random} Discussion NewMessageView`;
await waitFor(element(by.label(discussionName)).atIndex(0))
.toExist()
.withTimeout(5000);
await element(by.label(discussionName)).atIndex(0).tap();
await waitFor(element(by.id(`room-view-title-${discussionName}`)))
.toExist()
.withTimeout(5000);
await waitFor(element(by.id('messagebox')))
.toBeVisible()
.withTimeout(60000);
});
});
}); });

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,7 @@ import './Markdown';
import './HeaderButtons'; import './HeaderButtons';
import './UnreadBadge'; import './UnreadBadge';
import '../../app/views/ThreadMessagesView/Item.stories.js'; import '../../app/views/ThreadMessagesView/Item.stories.js';
import '../../app/views/DiscussionsView/Item.stories.js';
import './Avatar'; import './Avatar';
import './NewMarkdown'; import './NewMarkdown';
import '../../app/containers/BackgroundContainer/index.stories.js'; import '../../app/containers/BackgroundContainer/index.stories.js';