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:
name: Test
command: |
yarn test
yarn test -w 8
- run:
name: Codecov

View File

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

View File

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

View File

@ -35,7 +35,7 @@ const styles = StyleSheet.create({
const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => (
<View style={styles.container}>
<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}
</View>
);

View File

@ -36,7 +36,7 @@ interface IEmojiPickerProps {
}
interface IEmojiPickerState {
frequentlyUsed: [];
frequentlyUsed: (string | { content?: string; extension?: string; isCustom: boolean })[];
customEmojis: any;
show: boolean;
width: number | null;
@ -114,7 +114,7 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
// Do nothing
}
await db.action(async () => {
await db.write(async () => {
if (freqEmojiRecord) {
await freqEmojiRecord.update((f: any) => {
f.count += 1;
@ -132,8 +132,8 @@ class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
updateFrequentlyUsed = async () => {
const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
let frequentlyUsed: any = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
frequentlyUsed = frequentlyUsed.map((item: IEmoji) => {
const frequentlyUsedOrdered = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
const frequentlyUsed = frequentlyUsedOrdered.map(item => {
if (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 { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles';
import { TFrequentlyUsedEmojiModel } from '../../definitions/IFrequentlyUsedEmoji';
import { IEmoji } from '../EmojiPicker/interfaces';
interface IHeader {
@ -90,14 +91,14 @@ const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => (
));
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 setEmojis = async () => {
try {
const db = database.active;
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 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 Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events';
import { TMessageModel } from '../../definitions/IMessage';
interface IMessageActions {
room: {
@ -182,9 +183,9 @@ const MessageActions = React.memo(
if (result.success) {
const subCollection = db.get('subscriptions');
const subRecord = await subCollection.find(rid);
await db.action(async () => {
await db.write(async () => {
try {
await subRecord.update((sub: any) => (sub.lastOpen = ts));
await subRecord.update(sub => (sub.lastOpen = ts));
} catch {
// do nothing
}
@ -269,11 +270,11 @@ const MessageActions = React.memo(
}
};
const handleToggleTranslation = async (message: any) => {
const handleToggleTranslation = async (message: TMessageModel) => {
try {
const db = database.active;
await db.action(async () => {
await message.update((m: any) => {
await db.write(async () => {
await message.update(m => {
m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date();
});
@ -320,7 +321,7 @@ const MessageActions = React.memo(
});
};
const getOptions = (message: any) => {
const getOptions = (message: TMessageModel) => {
let options: any = [];
// Reply
@ -446,7 +447,7 @@ const MessageActions = React.memo(
return options;
};
const showMessageActions = async (message: any) => {
const showMessageActions = async (message: TMessageModel) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions();
showActionSheet({

View File

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

View File

@ -1,7 +1,8 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { withTheme } from '../theme';
import I18n from '../i18n';
import { useTheme } from '../theme';
import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors';
import TextInput from '../presentation/TextInput';
@ -19,14 +20,13 @@ const styles = StyleSheet.create({
}
});
interface ISearchHeader {
theme?: string;
interface ISearchHeaderProps {
onSearchChangeText?: (text: string) => void;
testID: string;
}
// TODO: it might be useful to refactor this component for reusage
const SearchHeader = ({ theme, onSearchChangeText }: ISearchHeader) => {
const titleColorStyle = { color: themes[theme!].headerTitleColor };
const SearchHeader = ({ onSearchChangeText, testID }: ISearchHeaderProps): JSX.Element => {
const { theme } = useTheme();
const isLight = theme === 'light';
const { isLandscape } = useOrientation();
const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1;
@ -36,14 +36,14 @@ const SearchHeader = ({ theme, onSearchChangeText }: ISearchHeader) => {
<View style={styles.container}>
<TextInput
autoFocus
style={[styles.title, isLight && titleColorStyle, { fontSize: titleFontSize }]}
placeholder='Search'
style={[styles.title, isLight && { color: themes[theme].headerTitleColor }, { fontSize: titleFontSize }]}
placeholder={I18n.t('Search')}
onChangeText={onSearchChangeText}
theme={theme!}
testID='thread-messages-view-search-header'
theme={theme}
testID={testID}
/>
</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 { themes } from '../constants/colors';
import sharedStyles from '../views/Styles';
import { withTheme } from '../theme';
import { useTheme } from '../theme';
import { TThreadModel } from '../definitions/IThread';
const styles = StyleSheet.create({
@ -48,12 +48,12 @@ interface IThreadDetails {
badgeColor?: string;
toggleFollowThread: Function;
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;
if (tcount! >= 1000) {
if (tcount && tcount >= 1000) {
tcount = '+999';
}
@ -81,7 +81,6 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style, them
</Text>
</View>
</View>
<View style={styles.badgeContainer}>
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null}
<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';
interface IMarkdownProps {
msg: string;
msg?: string;
md: MarkdownAST;
mentions: UserMention[];
getCustomEmoji: Function;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,12 +3,23 @@ import { StackNavigationProp } from '@react-navigation/stack';
import { Dispatch } from 'redux';
export * from './IAttachment';
export * from './IMessage';
export * from './INotification';
export * from './IRoom';
export * from './IServer';
export * from './ISubscription';
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 interface IBaseScreen<T extends Record<string, object | undefined>, S extends string> {

View File

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

View File

@ -25,6 +25,7 @@ import serversSchema from './schema/servers';
import appSchema from './schema/app';
import migrations from './model/migrations';
import serversMigrations from './model/servers/migrations';
import { TAppDatabase, TServerDatabase } from './interfaces';
const appGroupPath = isIOS ? appGroup.path : '';
@ -32,9 +33,9 @@ if (__DEV__ && isIOS) {
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 dbName = getDatabasePath(path);
@ -64,8 +65,14 @@ export const getDatabase = (database = '') => {
});
};
interface IDatabases {
shareDB?: TAppDatabase;
serversDB: TServerDatabase;
activeDB?: TAppDatabase;
}
class DB {
databases = {
databases: IDatabases = {
serversDB: new Database({
adapter: new SQLiteAdapter({
dbName: getDatabasePath('default'),
@ -73,11 +80,12 @@ class DB {
migrations: serversMigrations
}),
modelClasses: [Server, LoggedUser, ServersHistory]
})
}) as TServerDatabase
};
get active() {
return this.databases.shareDB || this.databases.activeDB;
// Expected at least one database
get active(): TAppDatabase {
return this.databases.shareDB || this.databases.activeDB!;
}
get share() {
@ -116,11 +124,11 @@ class DB {
Setting,
User
]
});
}) as TAppDatabase;
}
setActiveDB(database) {
this.databases.activeDB = getDatabase(database);
setActiveDB(database: string) {
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';
export const CUSTOM_EMOJIS_TABLE = 'custom_emojis';
export default class CustomEmoji extends Model {
static table = 'custom_emojis';
static table = CUSTOM_EMOJIS_TABLE;
@field('name') name;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,10 +3,10 @@ import { children, date, field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export const TABLE_NAME = 'subscriptions';
export const SUBSCRIPTIONS_TABLE = 'subscriptions';
export default class Subscription extends Model {
static table = TABLE_NAME;
static table = SUBSCRIPTIONS_TABLE;
static associations = {
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';
export const TABLE_NAME = 'threads';
export const THREADS_TABLE = 'threads';
export default class Thread extends Model {
static table = TABLE_NAME;
static table = THREADS_TABLE;
static associations = {
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';
export const TABLE_NAME = 'thread_messages';
export const THREAD_MESSAGES_TABLE = 'thread_messages';
export default class ThreadMessage extends Model {
static table = TABLE_NAME;
static table = THREAD_MESSAGES_TABLE;
static associations = {
subscriptions: { type: 'belongs_to', key: 'subscription_id' }

View File

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

View File

@ -3,8 +3,10 @@ import { field, json } from '@nozbe/watermelondb/decorators';
import { sanitizer } from '../utils';
export const USERS_TABLE = 'users';
export default class User extends Model {
static table = 'users';
static table = USERS_TABLE;
@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 { date, field } from '@nozbe/watermelondb/decorators';
export const SERVERS_TABLE = 'servers';
export default class Server extends Model {
static table = 'servers';
static table = SERVERS_TABLE;
@field('name') name;

View File

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

View File

@ -1,7 +1,7 @@
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 => {
const db = database.active;

View File

@ -1,7 +1,7 @@
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 => {
const db = database.active;

View File

@ -1,7 +1,7 @@
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 => {
const db = database.active;

View File

@ -1,7 +1,7 @@
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 => {
const db = database.active;

View File

@ -61,7 +61,7 @@ const PERMISSIONS = [
export async function setPermissions() {
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 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() {
const db = database.active;
const rolesCollection = db.collections.get('roles');
const rolesCollection = db.get('roles');
const allRoles = await rolesCollection.query().fetch();
const parsed = allRoles.reduce((acc, item) => ({ ...acc, [item.id]: item.description || item.id }), {});
reduxStore.dispatch(setRolesAction(parsed));

View File

@ -811,6 +811,16 @@ const RocketChat = {
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 }) {
const params = {
name,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons';
import sharedStyles from '../../Styles';
import Touch from '../../../utils/touch';
import { TServerHistory } from '../../../definitions/IServerHistory';
import { TServerHistoryModel } from '../../../definitions/IServerHistory';
const styles = StyleSheet.create({
container: {
@ -28,10 +28,10 @@ const styles = StyleSheet.create({
});
interface IItem {
item: TServerHistory;
item: TServerHistoryModel;
theme: string;
onPress(url: string): void;
onDelete(item: TServerHistory): void;
onDelete(item: TServerHistoryModel): void;
}
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 { themes } from '../../../constants/colors';
import I18n from '../../../i18n';
import { TServerHistory } from '../../../definitions/IServerHistory';
import { TServerHistoryModel } from '../../../definitions/IServerHistory';
import Item from './Item';
const styles = StyleSheet.create({
@ -33,8 +33,8 @@ interface IServerInput extends TextInputProps {
theme: string;
serversHistory: any[];
onSubmit(): void;
onDelete(item: TServerHistory): void;
onPressServerHistory(serverHistory: TServerHistory): void;
onDelete(item: TServerHistoryModel): void;
onPressServerHistory(serverHistory: TServerHistoryModel): void;
}
const ServerInput = ({

View File

@ -14,7 +14,7 @@ import Button from '../../containers/Button';
import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
import * as HeaderButton from '../../containers/HeaderButton';
import OrSeparator from '../../containers/OrSeparator';
import { IBaseScreen, TServerHistory } from '../../definitions';
import { IBaseScreen, TServerHistoryModel } from '../../definitions';
import { withDimensions } from '../../dimensions';
import I18n from '../../i18n';
import database from '../../lib/database';
@ -77,7 +77,7 @@ interface INewServerViewState {
text: string;
connectingOpen: boolean;
certificate: any;
serversHistory: TServerHistory[];
serversHistory: TServerHistoryModel[];
}
interface ISubmitParams {
@ -153,7 +153,7 @@ class NewServerView extends React.Component<INewServerViewProps, INewServerViewS
const likeString = sanitizeLikeString(text);
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 });
} catch {
// Do nothing
@ -177,7 +177,7 @@ class NewServerView extends React.Component<INewServerViewProps, INewServerViewS
dispatch(serverRequest(server));
};
onPressServerHistory = (serverHistory: TServerHistory) => {
onPressServerHistory = (serverHistory: TServerHistoryModel) => {
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;
try {
await db.write(async () => {
await item.destroyPermanently();
});
this.setState((prevstate: INewServerViewState) => ({
serversHistory: prevstate.serversHistory.filter((server: TServerHistory) => server.id !== item.id)
serversHistory: prevstate.serversHistory.filter(server => server.id !== item.id)
}));
} catch {
// Nothing

View File

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

View File

@ -643,7 +643,7 @@ class RoomActionsView extends React.Component {
const { addTeamChannelPermission, createTeamPermission } = this.props;
const QUERY_SIZE = 50;
const db = database.active;
const teams = await db.collections
const teams = await db
.get('subscriptions')
.query(
Q.where('team_main', true),
@ -941,7 +941,7 @@ class RoomActionsView extends React.Component {
canReturnQueue,
canViewCannedResponse
} = this.state;
const { rid, t } = room;
const { rid, t, prid } = room;
const isGroupChat = RocketChat.isGroupChat(room);
return (
@ -1009,6 +1009,27 @@ class RoomActionsView extends React.Component {
</>
) : 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) ? (
<>
<List.Item

View File

@ -144,11 +144,11 @@ class ListContainer extends React.Component {
if (tmid) {
try {
this.thread = await db.collections.get('threads').find(tmid);
this.thread = await db.get('threads').find(tmid);
} catch (e) {
console.log(e);
}
this.messagesObservable = db.collections
this.messagesObservable = db
.get('thread_messages')
.query(Q.where('rid', tmid), Q.experimentalSortBy('ts', Q.desc), Q.experimentalSkip(0), Q.experimentalTake(this.count))
.observe();
@ -162,7 +162,7 @@ class ListContainer extends React.Component {
if (!showMessageInMainThread) {
whereClause.push(Q.or(Q.where('tmid', null), Q.where('tshow', Q.eq(true))));
}
this.messagesObservable = db.collections
this.messagesObservable = db
.get('messages')
.query(...whereClause)
.observe();

View File

@ -91,7 +91,7 @@ class UploadProgress extends Component {
}
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 => {
if (this.mounted) {

View File

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

View File

@ -47,7 +47,7 @@ class ServerDropdown extends Component {
async componentDidMount() {
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.setState({ servers: data });

View File

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

View File

@ -92,7 +92,7 @@ class ScreenLockConfigView extends React.Component<IScreenLockConfigViewProps, I
const serversDB = database.servers;
const serversCollection = serversDB.get('servers');
try {
this.serverRecord = (await serversCollection.find(server)) as TServerModel;
this.serverRecord = await serversCollection.find(server);
this.setState({
autoLock: this.serverRecord?.autoLock,
autoLockTime: this.serverRecord?.autoLockTime === null ? DEFAULT_AUTO_LOCK : this.serverRecord?.autoLockTime,
@ -115,7 +115,7 @@ class ScreenLockConfigView extends React.Component<IScreenLockConfigViewProps, I
*/
observe = () => {
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 () => {
try {
const db = database.active;
const observable = await db.collections
.get('subscriptions')
.query(Q.where('t', 'd'))
.observeWithColumns(['room_updated_at']);
const observable = await db.get('subscriptions').query(Q.where('t', 'd')).observeWithColumns(['room_updated_at']);
// TODO: Refactor when migrate room
this.querySubscription = observable.subscribe((data: any) => {
@ -129,7 +126,9 @@ class SelectedUsersView extends React.Component<ISelectedUsersViewProps, ISelect
}
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({
search: result
});

View File

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

View File

@ -2,12 +2,12 @@
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { ScrollView } from 'react-native';
import { combineReducers, createStore } from 'redux';
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 = {
@ -49,28 +49,6 @@ const listDecorator = story => (
</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)
.addDecorator(listDecorator)
.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 Touchable from 'react-native-platform-touchable';
import { withTheme } from '../../theme';
import { useTheme } from '../../theme';
import Avatar from '../../containers/Avatar';
import sharedStyles from '../Styles';
import { themes } from '../../constants/colors';
@ -59,7 +59,6 @@ const styles = StyleSheet.create({
interface IItem {
item: TThreadModel;
baseUrl: string;
theme?: string;
useRealName: boolean;
user: any;
badgeColor?: string;
@ -67,7 +66,8 @@ interface IItem {
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;
let time;
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>
</View>
<View style={styles.messageContainer}>
{makeThreadName(item) && username ? (
/* @ts-ignore */
<Markdown
// @ts-ignore
msg={makeThreadName(item)}
baseUrl={baseUrl}
username={username!}
theme={theme!}
username={username}
theme={theme}
numberOfLines={2}
style={[styles.markdown]}
preview
/>
) : null}
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null}
</View>
<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 { RouteProp } from '@react-navigation/native';
import { Observable, Subscription } from 'rxjs';
import Model from '@nozbe/watermelondb/Model';
import Database from '@nozbe/watermelondb/Database';
import ActivityIndicator from '../../containers/ActivityIndicator';
@ -86,7 +85,7 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
private messagesSubscription?: Subscription;
private messagesObservable!: Observable<Model>;
private messagesObservable?: Observable<TThreadModel[]>;
constructor(props: IThreadMessagesViewProps) {
super(props);
@ -144,7 +143,9 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
<HeaderButton.Item iconName='close' onPress={this.onCancelSearchPress} />
</HeaderButton.Container>
),
headerTitle: () => <SearchHeader onSearchChangeText={this.onSearchChangeText} />,
headerTitle: () => (
<SearchHeader onSearchChangeText={this.onSearchChangeText} testID='thread-messages-view-search-header' />
),
headerTitleContainerStyle: {
left: headerTitlePosition.left,
right: headerTitlePosition.right
@ -188,7 +189,7 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
const db = database.active;
// 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();
this.subSubscription = observable.subscribe(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())}%`)));
}
this.messagesObservable = db.collections
this.messagesObservable = db
.get('threads')
.query(...whereClause)
.observeWithColumns(['updated_at']);
// TODO: Refactor when migrate messages
this.messagesSubscription = this.messagesObservable.subscribe((messages: any) => {
this.messagesSubscription = this.messagesObservable.subscribe(messages => {
const { currentFilter } = this.state;
const displayingThreads = this.getFilteredThreads(messages, subscription!, currentFilter);
const displayingThreads = this.getFilteredThreads(messages, subscription, currentFilter);
if (this.mounted) {
this.setState({ messages, displayingThreads });
} else {
@ -419,14 +420,14 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
};
// 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 { user } = this.props;
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) {
return messages?.filter((item: { id: string }) => subscription?.tunread?.includes(item?.id));
return messages?.filter(item => subscription?.tunread?.includes(item?.id));
}
return messages;
};

View File

@ -107,7 +107,7 @@ describe('Discussion', () => {
});
describe('Check RoomActionsView render', () => {
it('should navigete to RoomActionsView', async () => {
it('should navigate to RoomActionsView', async () => {
await waitFor(element(by.id('room-header')))
.toBeVisible()
.withTimeout(5000);
@ -173,4 +173,51 @@ describe('Discussion', () => {
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 './UnreadBadge';
import '../../app/views/ThreadMessagesView/Item.stories.js';
import '../../app/views/DiscussionsView/Item.stories.js';
import './Avatar';
import './NewMarkdown';
import '../../app/containers/BackgroundContainer/index.stories.js';