[Gekidou] Find channels (remote) (#6203)

* Display local results

* Fix queryPreferencesByCategoryAndName to observeWithColumns value

* Find channels (remote)

* ux feedback review

* dev review

* dev review 2

* Fetch deleted channels from other teams
This commit is contained in:
Elias Nahum 2022-05-03 14:29:37 -04:00 committed by GitHub
parent bb42339c42
commit f973ac8016
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 899 additions and 100 deletions

View file

@ -386,7 +386,7 @@ describe('switchToChannel', () => {
const listenerCallback = jest.fn();
const listener = DeviceEventEmitter.addListener(Navigation.NAVIGATION_HOME, listenerCallback);
const {models, error} = await switchToChannel(serverUrl, channelId, teamId, true);
const {models, error} = await switchToChannel(serverUrl, channelId, teamId, false, true);
for (const model of models!) {
model.cancelPrepareUpdate();
}

View file

@ -26,7 +26,7 @@ import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from
import type ChannelModel from '@typings/database/models/servers/channel';
import type UserModel from '@typings/database/models/servers/user';
export async function switchToChannel(serverUrl: string, channelId: string, teamId?: string, prepareRecordsOnly = false) {
export async function switchToChannel(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false, prepareRecordsOnly = false) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
@ -63,7 +63,7 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
}
const commonValues: PrepareCommonSystemValuesArgs = {
lastUnreadChannelId: member.isUnread ? channelId : '',
lastUnreadChannelId: member.isUnread && !skipLastUnread ? channelId : '',
};
if ((system.currentTeamId !== toTeamId) || (system.currentChannelId !== channelId)) {

View file

@ -16,7 +16,7 @@ import NetworkManager from '@managers/network_manager';
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
import {prepareMyTeams, getNthLastChannelFromTeam, getMyTeamById, getTeamById, getTeamByName} from '@queries/servers/team';
import {prepareMyTeams, getNthLastChannelFromTeam, getMyTeamById, getTeamById, getTeamByName, queryMyTeams} from '@queries/servers/team';
import {getCurrentUser} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel';
@ -440,10 +440,12 @@ export async function joinChannel(serverUrl: string, userId: string, teamId: str
let channel: Channel | undefined;
try {
if (channelId) {
EphemeralStore.addJoiningChannel(channelId);
member = await client.addToChannel(userId, channelId);
channel = await client.getChannel(channelId);
} else if (channelName) {
channel = await client.getChannelByName(teamId, channelName, true);
EphemeralStore.addJoiningChannel(channel.id);
if (isDMorGM(channel)) {
member = await client.getChannelMember(channel.id, userId);
} else {
@ -451,6 +453,9 @@ export async function joinChannel(serverUrl: string, userId: string, teamId: str
}
}
} catch (error) {
if (channelId || channel?.id) {
EphemeralStore.removeJoiningChanel(channelId || channel!.id);
}
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
@ -478,12 +483,37 @@ export async function joinChannel(serverUrl: string, userId: string, teamId: str
}
}
} catch (error) {
if (channelId || channel?.id) {
EphemeralStore.removeJoiningChanel(channelId || channel!.id);
}
return {error};
}
if (channelId || channel?.id) {
EphemeralStore.removeJoiningChanel(channelId || channel!.id);
}
return {channel, member};
}
export async function joinChannelIfNeeded(serverUrl: string, channelId: string) {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
try {
const myChannel = await getMyChannel(database, channelId);
if (myChannel) {
return {error: undefined};
}
const userId = await getCurrentUserId(database);
return joinChannel(serverUrl, userId, '', channelId);
} catch (error) {
return {error};
}
}
export async function markChannelAsRead(serverUrl: string, channelId: string) {
try {
const client = NetworkManager.getClient(serverUrl);
@ -933,14 +963,14 @@ export async function getChannelTimezones(serverUrl: string, channelId: string)
}
}
export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string) {
export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false) {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
fetchPostsForChannel(serverUrl, channelId);
await switchToChannel(serverUrl, channelId, teamId);
await switchToChannel(serverUrl, channelId, teamId, skipLastUnread);
markChannelAsRead(serverUrl, channelId);
fetchChannelStats(serverUrl, channelId);
@ -1002,3 +1032,25 @@ export async function fetchChannelById(serverUrl: string, id: string) {
return {error};
}
}
export async function searchAllChannels(serverUrl: string, term: string, archivedOnly = false) {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const myTeamIds = await queryMyTeams(database).fetchIds();
const channels = await client.searchAllChannels(term, myTeamIds, archivedOnly);
return {channels};
} catch (error) {
return {error};
}
}

View file

@ -196,7 +196,7 @@ export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, since:
const myTeams = teams.filter((t) => membershipSet.has(t.id) && t.id !== excludeTeamId);
for await (const team of myTeams) {
const {channels, memberships: members} = await fetchMyChannelsForTeam(serverUrl, team.id, since > 0, since, false, true);
const {channels, memberships: members} = await fetchMyChannelsForTeam(serverUrl, team.id, true, since, false, true);
if (channels?.length && members?.length) {
fetchPostsForUnreadChannels(serverUrl, channels, members);

View file

@ -420,7 +420,11 @@ export const searchProfiles = async (serverUrl: string, term: string, options: a
const users = await client.searchUsers(term, options);
if (!fetchOnly) {
const toStore = removeUserFromList(currentUserId, users);
const {database} = operator;
const existing = await queryUsersById(database, users.map((u) => u.id)).fetchIds();
const existingSet = new Set(existing);
const usersToAdd = users.filter((u) => !existingSet.has(u.id));
const toStore = removeUserFromList(currentUserId, usersToAdd);
if (toStore.length) {
await operator.handleUsers({
users: toStore,

View file

@ -251,9 +251,10 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
try {
if (userId === currentUser?.id) {
if (EphemeralStore.isAddingToTeam(teamId)) {
if (EphemeralStore.isAddingToTeam(teamId) || EphemeralStore.isJoiningChannel(channelId)) {
return;
}
const {channels, memberships} = await fetchMyChannel(serverUrl, teamId, channelId, true);
if (channels && memberships) {
const prepare = await prepareMyChannelsForTeam(operator, teamId, channels, memberships);
@ -300,7 +301,10 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
}));
}
}
await operator.batchRecords(models);
if (models.length) {
await operator.batchRecords(models);
}
await fetchChannelStats(serverUrl, channelId, false);
} catch {
@ -358,7 +362,7 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
models.push(...switchToGlobalThreadsModels);
}
} else {
const {models: switchChannelModels} = await switchToChannel(serverUrl, channelToJumpTo, '', true);
const {models: switchChannelModels} = await switchToChannel(serverUrl, channelToJumpTo, '', false, true);
if (switchChannelModels) {
models.push(...switchChannelModels);
}

View file

@ -39,6 +39,7 @@ export interface ClientChannelsMix {
autocompleteChannelsForSearch: (teamId: string, name: string) => Promise<Channel[]>;
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
searchArchivedChannels: (teamId: string, term: string) => Promise<Channel[]>;
searchAllChannels: (term: string, teamIds: string[], archivedOnly?: boolean) => Promise<Channel[]>;
}
const ClientChannels = (superclass: any) => class extends superclass {
@ -313,6 +314,24 @@ const ClientChannels = (superclass: any) => class extends superclass {
{method: 'post', body: {term}},
);
};
searchAllChannels = async (term: string, teamIds: string[], archivedOnly = false) => {
const queryParams = {include_deleted: false, system_console: false, exclude_default_channels: false};
const body = {
term,
team_ids: teamIds,
deleted: archivedOnly,
exclude_default_channels: true,
exclude_group_constrained: true,
public: true,
private: false,
};
return this.doFetch(
`${this.getChannelsRoute()}/search${buildQueryString(queryParams)}`,
{method: 'post', body},
);
};
};
export default ClientChannels;

View file

@ -26,9 +26,10 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
of$(emptyEmojiList)),
),
),
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).observe().pipe(
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
),
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
observeWithColumns(['value']).pipe(
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
),
};
});

View file

@ -175,7 +175,7 @@ const ChannelListItem = ({
],
[height, isActive, isInfo, styles]);
if ((channel.deleteAt > 0 && !isActive) || !myChannel || !isVisible) {
if (!myChannel || !isVisible) {
return null;
}
@ -229,7 +229,12 @@ const ChannelListItem = ({
</Text>
}
</View>
{Boolean(teammateId) && <CustomStatus userId={teammateId!}/>}
{Boolean(teammateId) &&
<CustomStatus
isInfo={isInfo}
userId={teammateId!}
/>
}
{isInfo && Boolean(teamDisplayName) && isTablet &&
<Text
ellipsizeMode='tail'

View file

@ -10,17 +10,21 @@ type Props = {
customStatus?: UserCustomStatus;
customStatusExpired: boolean;
isCustomStatusEnabled: boolean;
isInfo?: boolean;
}
const style = StyleSheet.create({
customStatusEmoji: {
color: '#000',
marginHorizontal: 5,
marginTop: 1,
top: -4,
},
info: {
marginHorizontal: -15,
},
});
const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled}: Props) => {
const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled, isInfo}: Props) => {
const showCustomStatusEmoji = Boolean(isCustomStatusEnabled && customStatus && !customStatusExpired);
if (!showCustomStatusEmoji) {
@ -30,7 +34,7 @@ const CustomStatus = ({customStatus, customStatusExpired, isCustomStatusEnabled}
return (
<CustomStatusEmoji
customStatus={customStatus!}
style={style.customStatusEmoji}
style={[style.customStatusEmoji, isInfo && style.info]}
testID={`channel_list_item.custom_status.${customStatus!.emoji}-${customStatus!.text}`}
/>
);

View file

@ -2,14 +2,14 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Text, TextStyle} from 'react-native';
import {StyleProp, Text, TextStyle} from 'react-native';
import Emoji from '@components/emoji';
interface ComponentProps {
customStatus: UserCustomStatus;
emojiSize?: number;
style?: TextStyle;
style?: StyleProp<TextStyle>;
testID?: string;
}

View file

@ -126,11 +126,12 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
};
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
isMilitaryTime: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).observe().pipe(
switchMap(
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
isMilitaryTime: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
observeWithColumns(['value']).pipe(
switchMap(
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
),
),
),
}));
export default withDatabase(enhanced(CustomStatusExpiry));

View file

@ -129,9 +129,10 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
customEmojisEnabled: observeConfigBooleanValue(database, 'EnableCustomEmoji'),
customEmojis: queryAllCustomEmojis(database).observe(),
recentEmojis: observeRecentReactions(database),
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).observe().pipe(
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
),
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
observeWithColumns(['value']).pipe(
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
),
}));
export default withDatabase(enhanced(EmojiPicker));

View file

@ -73,7 +73,7 @@ const ChannelMention = ({
if (result.error || !result.channel) {
const joinFailedMessage = {
id: t('mobile.join_channel.error'),
defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again.",
defaultMessage: "We couldn't join the channel {displayName}.",
};
alertErrorWithFallback(intl, result.error || {}, joinFailedMessage, {displayName: c.display_name});
} else if (result.channel) {

View file

@ -22,7 +22,8 @@ const enhance = withObservables(
}
const config = observeConfig(database);
const linkPreviewPreference = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY).observe();
const linkPreviewPreference = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY).
observeWithColumns(['value']);
const showLinkPreviews = combineLatest([config, linkPreviewPreference], (cfg, pref) => {
const previewsEnabled = getPreferenceAsBool(pref, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY, true);
return of$(previewsEnabled && cfg?.EnableLinkPreviews === 'true');

View file

@ -28,7 +28,8 @@ const withHeaderProps = withObservables(
({post, database, differentThreadSequence}: WithDatabaseArgs & HeaderInputProps) => {
const config = observeConfig(database);
const license = observeLicense(database);
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).observe();
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
observeWithColumns(['value']);
const author = post.author.observe();
const enablePostUsernameOverride = config.pipe(map((cfg) => cfg?.EnablePostUsernameOverride === 'true'));
const isTimezoneEnabled = config.pipe(map((cfg) => cfg?.ExperimentalTimezone === 'true'));

View file

@ -103,9 +103,10 @@ const withPost = withObservables(
const author = post.userId ? post.author.observe() : of$(null);
const canDelete = observePermissionForPost(post, currentUser, isOwner ? Permissions.DELETE_POST : Permissions.DELETE_OTHERS_POSTS, false);
const isEphemeral = of$(isPostEphemeral(post));
const isSaved = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SAVED_POST, post.id).observe().pipe(
switchMap((pref) => of$(Boolean(pref.length))),
);
const isSaved = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SAVED_POST, post.id).
observeWithColumns(['value']).pipe(
switchMap((pref) => of$(Boolean(pref.length))),
);
if (post.props?.add_channel_member && isPostEphemeral(post)) {
isPostAddChannelMember = observeCanManageChannelMembers(post, currentUser);

View file

@ -20,7 +20,7 @@ const enhanced = withObservables(
return {
rootPost: observePost(database, rootId),
isSaved: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SAVED_POST, rootId).
observe().
observeWithColumns(['value']).
pipe(
switchMap((pref) => of$(Boolean(pref[0]?.value === 'true'))),
),

View file

@ -81,7 +81,8 @@ const SystemHeader = ({isMilitaryTime, isTimezoneEnabled, createAt, theme, user}
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const config = observeConfig(database);
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time').observe();
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time').
observeWithColumns(['value']);
const isTimezoneEnabled = config.pipe(map((cfg) => cfg?.ExperimentalTimezone === 'true'));
const isMilitaryTime = preferences.pipe(
map((prefs) => getPreferenceAsBool(prefs, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),

View file

@ -21,9 +21,10 @@ const withTeams = withObservables([], ({database}: WithDatabaseArgs) => {
const teamIds = queryJoinedTeams(database).observe().pipe(
map((ts) => ts.map((t) => ({id: t.id, displayName: t.displayName}))),
);
const order = queryPreferencesByCategoryAndName(database, Preferences.TEAMS_ORDER).observe().pipe(
switchMap((p) => (p.length ? of$(p[0].value.split(',')) : of$([]))),
);
const order = queryPreferencesByCategoryAndName(database, Preferences.TEAMS_ORDER).
observeWithColumns(['value']).pipe(
switchMap((p) => (p.length ? of$(p[0].value.split(',')) : of$([]))),
);
const myOrderedTeams = combineLatest([myTeams, order, teamIds]).pipe(
map(([ts, o, tids]) => {
let ids: string[] = o;

View file

@ -11,7 +11,8 @@ import {hasPermission} from '@utils/role';
import {prepareDeletePost} from './post';
import {queryRoles} from './role';
import {observeCurrentChannelId, getCurrentChannelId} from './system';
import {observeCurrentChannelId, getCurrentChannelId, observeCurrentUserId} from './system';
import {observeTeammateNameDisplay} from './user';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type ChannelModel from '@typings/database/models/servers/channel';
@ -431,3 +432,103 @@ export function queryMyChannelsByUnread(database: Database, isUnread: boolean, s
Q.take(take),
);
}
export const observeDirectChannelsByTerm = (database: Database, term: string, take = 20, matchStart = false) => {
const onlyDMs = term.startsWith('@') ? "AND c.type='D'" : '';
const value = Q.sanitizeLikeString(term.startsWith('@') ? term.substring(1) : term);
let username = `u.username LIKE '${value}%'`;
let displayname = `c.display_name LIKE '${value}%'`;
if (!matchStart) {
username = `u.username LIKE '%${value}%' AND u.username NOT LIKE '${value}%'`;
displayname = `(c.display_name LIKE '%${value}%' AND c.display_name NOT LIKE '${value}%')`;
}
const currentUserId = observeCurrentUserId(database);
return currentUserId.pipe(
switchMap((uId) => {
return database.get<MyChannelModel>(CHANNEL).query(
Q.unsafeSqlQuery(`SELECT DISTINCT my.* FROM ${MY_CHANNEL} my
INNER JOIN ${CHANNEL} c ON c.id=my.id AND c.team_id='' AND c.delete_at=0 ${onlyDMs}
INNER JOIN ${CHANNEL_MEMBERSHIP} cm ON cm.channel_id=my.id
INNER JOIN ${USER} u ON u.id=cm.user_id AND (cm.user_id != '${uId}' AND ${username})
OR ${displayname}
ORDER BY my.last_viewed_at DESC
LIMIT ${take}`),
).observe();
}),
);
};
export const observeNotDirectChannelsByTerm = (database: Database, term: string, take = 20, matchStart = false) => {
const teammateNameSetting = observeTeammateNameDisplay(database);
const value = Q.sanitizeLikeString(term.startsWith('@') ? term.substring(1) : term);
let username = `u.username LIKE '${value}%'`;
let nickname = `u.nickname LIKE '${value}%'`;
let displayname = `(u.first_name || ' ' || u.last_name) LIKE '${value}%'`;
if (!matchStart) {
username = `(u.username LIKE '%${value}%' AND u.username NOT LIKE '${value}%')`;
nickname = `(u.nickname LIKE '%${value}%' AND u.nickname NOT LIKE '${value}%')`;
displayname = `((u.first_name || ' ' || u.last_name) LIKE '%${value}%' AND (u.first_name || ' ' || u.last_name) NOT LIKE '${value}%')`;
}
return teammateNameSetting.pipe(
switchMap((setting) => {
let sortBy = '';
switch (setting) {
case General.TEAMMATE_NAME_DISPLAY.SHOW_NICKNAME_FULLNAME:
sortBy = "ORDER BY CASE u.nickname WHEN '' THEN 1 ELSE 0 END, CASE (u.first_name || ' ' || u.last_name) WHEN '' THEN 1 ELSE 0 END, u.username";
break;
case General.TEAMMATE_NAME_DISPLAY.SHOW_FULLNAME:
sortBy = "ORDER BY CASE (u.first_name || ' ' || u.last_name) WHEN '' THEN 1 ELSE 0 END, u.username";
break;
default:
sortBy = 'ORDER BY u.username';
break;
}
return database.get<UserModel>(USER).query(
Q.unsafeSqlQuery(`SELECT DISTINCT u.* FROM User u
LEFT JOIN ChannelMembership cm ON cm.user_id=u.id
LEFT JOIN Channel c ON c.id=cm.id AND c.type='${General.DM_CHANNEL}'
WHERE cm.user_id IS NULL AND (${displayname} OR ${username} OR ${nickname})
${sortBy} LIMIT ${take}`),
).observe();
}),
);
};
export const observeJoinedChannelsByTerm = (database: Database, term: string, take = 20, matchStart = false) => {
if (term.startsWith('@')) {
return of$([]);
}
const value = Q.sanitizeLikeString(term);
let displayname = `c.display_name LIKE '${value}%'`;
if (!matchStart) {
displayname = `c.display_name LIKE '%${value}%' AND c.display_name NOT LIKE '${value}%'`;
}
return database.get<MyChannelModel>(MY_CHANNEL).query(
Q.unsafeSqlQuery(`SELECT DISTINCT my.* FROM ${MY_CHANNEL} my
INNER JOIN ${CHANNEL} c ON c.id=my.id AND c.delete_at=0 AND c.team_id !='' AND ${displayname}
ORDER BY my.last_viewed_at DESC
LIMIT ${take}`),
).observe();
};
export const observeArchiveChannelsByTerm = (database: Database, term: string, take = 20) => {
if (term.startsWith('@')) {
return of$([]);
}
const value = Q.sanitizeLikeString(term);
const displayname = `%${value}%`;
return database.get<MyChannelModel>(MY_CHANNEL).query(
Q.on(CHANNEL, Q.and(
Q.where('delete_at', Q.gt(0)),
Q.where('team_id', Q.notEq('')),
Q.where('display_name', Q.like(displayname)),
)),
Q.sortBy('last_viewed_at'),
Q.take(take),
).observe();
};

View file

@ -55,11 +55,12 @@ export const observePost = (database: Database, postId: string) => {
};
export const observePostSaved = (database: Database, postId: string) => {
return queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SAVED_POST, postId).observe().pipe(
switchMap(
(pref) => of$(Boolean(pref[0]?.value === 'true')),
),
);
return queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SAVED_POST, postId).
observeWithColumns(['value']).pipe(
switchMap(
(pref) => of$(Boolean(pref[0]?.value === 'true')),
),
);
};
export const queryPostsInChannel = (database: Database, channelId: string) => {

View file

@ -65,7 +65,8 @@ export async function prepareUsers(operator: ServerDataOperator, users: UserProf
export const observeTeammateNameDisplay = (database: Database) => {
const config = observeConfig(database);
const license = observeLicense(database);
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).observe();
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
observeWithColumns(['value']);
return combineLatest([config, license, preferences]).pipe(
switchMap(
([cfg, lcs, prefs]) => of$(getTeammateNameDisplaySetting(prefs, cfg, lcs)),

View file

@ -47,9 +47,10 @@ const enhanced = withObservables(['channelId', 'forceQueryAfterAppState'], ({dat
return queryPostsBetween(database, earliest, latest, Q.desc, '', channelId, isCRTEnabled ? '' : undefined).observe();
}),
),
shouldShowJoinLeaveMessages: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE).observe().pipe(
switchMap((preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE, true))),
),
shouldShowJoinLeaveMessages: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE).
observeWithColumns(['value']).pipe(
switchMap((preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_ADVANCED_SETTINGS, Preferences.ADVANCED_FILTER_JOIN_LEAVE, true))),
),
};
});

View file

@ -23,7 +23,8 @@ const enhanced = withObservables([], ({channel, database}: {channel: ChannelMode
if (channel.creatorId) {
const config = observeConfig(database);
const license = observeLicense(database);
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).observe();
const preferences = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
observeWithColumns(['value']);
const me = observeCurrentUser(database);
const profile = observeUser(database, channel.creatorId);

View file

@ -113,11 +113,12 @@ const DateTimeSelector = ({timezone, handleChange, isMilitaryTime, theme}: Props
};
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
isMilitaryTime: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).observe().pipe(
switchMap(
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
isMilitaryTime: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
observeWithColumns(['value']).pipe(
switchMap(
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
),
),
),
}));
export default withDatabase(enhanced(DateTimeSelector));

View file

@ -1,26 +1,59 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo, useState} from 'react';
import {FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
import {debounce, DebouncedFunc} from 'lodash';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Alert, FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
import Animated, {FadeInDown, FadeOutUp} from 'react-native-reanimated';
import {switchToChannelById} from '@actions/remote/channel';
import {joinChannelIfNeeded, makeDirectChannel, searchAllChannels, switchToChannelById} from '@actions/remote/channel';
import {searchProfiles} from '@actions/remote/user';
import ChannelItem from '@components/channel_item';
import Loading from '@components/loading';
import NoResultsWithTerm from '@components/no_results_with_term';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {sortChannelsByDisplayName} from '@utils/channel';
import {displayUsername} from '@utils/user';
import RemoteChannelItem from './remote_channel_item';
import UserItem from './user_item';
import type ChannelModel from '@typings/database/models/servers/channel';
import type UserModel from '@typings/database/models/servers/user';
type RemoteChannels = {
archived: Channel[];
startWith: Channel[];
matches: Channel[];
}
type Props = {
archivedChannels: ChannelModel[];
close: () => Promise<void>;
channelsMatch: ChannelModel[];
channelsMatchStart: ChannelModel[];
currentTeamId: string;
keyboardHeight: number;
loading: boolean;
onLoading: (loading: boolean) => void;
restrictDirectMessage: boolean;
showTeamName: boolean;
teamIds: Set<string>;
teammateDisplayNameSetting: string;
term: string;
usersMatch: UserModel[];
usersMatchStart: UserModel[];
}
const style = StyleSheet.create({
flex: {flex: 1},
loading: {
height: 32,
width: 32,
justifyContent: 'center',
},
noResultContainer: {
flexGrow: 1,
alignItems: 'center',
@ -28,17 +61,138 @@ const style = StyleSheet.create({
},
});
const UnfilteredList = ({close, keyboardHeight, showTeamName, term}: Props) => {
const serverUrl = useServerUrl();
const [data] = useState([]);
const flatListStyle = useMemo(() => ({flexGrow: 1, paddingBottom: keyboardHeight}), [keyboardHeight]);
export const MAX_RESULTS = 20;
const onPress = useCallback(async (channelId: string) => {
const sortByLastPostAt = (a: Channel, b: Channel) => {
return a.last_post_at > b.last_post_at ? 1 : -1;
};
const sortByUserOrChannel = <T extends Channel |UserModel>(locale: string, teammateDisplayNameSetting: string, a: T, b: T): number => {
const aDisplayName = 'display_name' in a ? a.display_name : displayUsername(a, locale, teammateDisplayNameSetting);
const bDisplayName = 'display_name' in b ? b.display_name : displayUsername(b, locale, teammateDisplayNameSetting);
return aDisplayName.toLowerCase().localeCompare(bDisplayName.toLowerCase(), locale, {numeric: true});
};
const FilteredList = ({
archivedChannels, close, channelsMatch, channelsMatchStart, currentTeamId,
keyboardHeight, loading, onLoading, restrictDirectMessage, showTeamName,
teamIds, teammateDisplayNameSetting, term, usersMatch, usersMatchStart,
}: Props) => {
const bounce = useRef<DebouncedFunc<() => void>>();
const mounted = useRef(false);
const serverUrl = useServerUrl();
const theme = useTheme();
const {locale, formatMessage} = useIntl();
const flatListStyle = useMemo(() => ({flexGrow: 1, paddingBottom: keyboardHeight}), [keyboardHeight]);
const [remoteChannels, setRemoteChannels] = useState<RemoteChannels>({archived: [], startWith: [], matches: []});
const totalLocalResults = channelsMatchStart.length + channelsMatch.length + usersMatchStart.length;
const search = async () => {
onLoading(true);
if (mounted.current) {
setRemoteChannels({archived: [], startWith: [], matches: []});
}
const lowerCasedTerm = (term.startsWith('@') ? term.substring(1) : term).toLowerCase();
if ((channelsMatchStart.length + channelsMatch.length) < MAX_RESULTS) {
if (restrictDirectMessage) {
searchProfiles(serverUrl, lowerCasedTerm, {team_id: currentTeamId, allow_inactive: true});
} else {
searchProfiles(serverUrl, lowerCasedTerm, {allow_inactive: true});
}
}
if (!term.startsWith('@')) {
if (totalLocalResults < MAX_RESULTS) {
const {channels} = await searchAllChannels(serverUrl, lowerCasedTerm, true);
if (channels) {
const existingChannelIds = new Set(channelsMatchStart.concat(channelsMatch).concat(archivedChannels).map((c) => c.id));
const [startWith, matches, archived] = channels.reduce<[Channel[], Channel[], Channel[]]>(([s, m, a], c) => {
if (existingChannelIds.has(c.id) || !teamIds.has(c.team_id)) {
return [s, m, a];
}
if (!c.delete_at) {
if (c.display_name.toLowerCase().startsWith(lowerCasedTerm)) {
return [[...s, c], m, a];
}
if (c.display_name.toLowerCase().includes(lowerCasedTerm)) {
return [s, [...m, c], a];
}
return [s, m, a];
}
if (c.display_name.toLowerCase().includes(lowerCasedTerm)) {
return [s, m, [...a, c]];
}
return [s, m, a];
}, [[], [], []]);
if (mounted.current) {
setRemoteChannels({
archived: archived.sort(sortChannelsByDisplayName.bind(null, locale)).slice(0, MAX_RESULTS + 1),
startWith: startWith.sort(sortByLastPostAt).slice(0, MAX_RESULTS + 1),
matches: matches.sort(sortChannelsByDisplayName.bind(null, locale)).slice(0, MAX_RESULTS + 1),
});
}
}
}
}
onLoading(false);
};
const onJoinChannel = useCallback(async (channelId: string, displayName: string) => {
const {error} = await joinChannelIfNeeded(serverUrl, channelId);
if (error) {
Alert.alert(
'',
formatMessage({
id: 'mobile.join_channel.error',
defaultMessage: "We couldn't join the channel {displayName}.",
}, {displayName}),
);
return;
}
await close();
switchToChannelById(serverUrl, channelId, undefined, true);
}, [serverUrl, close, locale]);
const onOpenDirectMessage = useCallback(async (teammateId: string, displayName: string) => {
const {data, error} = await makeDirectChannel(serverUrl, teammateId, displayName, false);
if (error || !data) {
Alert.alert(
'',
formatMessage({
id: 'mobile.direct_message.error',
defaultMessage: "We couldn't open a DM with {displayName}.",
}, {displayName}),
);
return;
return;
}
await close();
switchToChannelById(serverUrl, data.id);
}, [serverUrl, close, locale]);
const onSwitchToChannel = useCallback(async (channelId: string) => {
await close();
switchToChannelById(serverUrl, channelId);
}, [serverUrl, close]);
const renderNoResults = useCallback(() => {
const renderEmpty = useCallback(() => {
if (loading) {
return (
<Loading
containerStyle={style.noResultContainer}
style={style.loading}
color={theme.buttonBg}
/>
);
}
if (term) {
return (
<View style={style.noResultContainer}>
@ -48,19 +202,88 @@ const UnfilteredList = ({close, keyboardHeight, showTeamName, term}: Props) => {
}
return null;
}, [term]);
}, [term, loading, theme]);
const renderItem = useCallback(({item}: ListRenderItemInfo<ChannelModel|Channel|UserModel>) => {
if ('teamId' in item) {
return (
<ChannelItem
channel={item}
collapsed={false}
isInfo={true}
onPress={onSwitchToChannel}
showTeamName={showTeamName}
/>
);
} else if ('username' in item) {
return (
<UserItem
onPress={onOpenDirectMessage}
user={item}
/>
);
}
const renderItem = useCallback(({item}: ListRenderItemInfo<ChannelModel>) => {
return (
<ChannelItem
<RemoteChannelItem
channel={item}
collapsed={false}
isInfo={true}
onPress={onPress}
onPress={onJoinChannel}
showTeamName={showTeamName}
/>
);
}, [onPress, showTeamName]);
}, [onJoinChannel, onOpenDirectMessage, onSwitchToChannel, showTeamName, teammateDisplayNameSetting]);
const data = useMemo(() => {
const items: Array<ChannelModel|Channel|UserModel> = [...channelsMatchStart];
// Channels that matches
if (items.length < MAX_RESULTS) {
items.push(...channelsMatch);
}
// Users that start with
if (items.length < MAX_RESULTS) {
items.push(...usersMatchStart);
}
// Remote Channels that start with
if (items.length < MAX_RESULTS) {
items.push(...remoteChannels.startWith);
}
// Users & Channels that matches
if (items.length < MAX_RESULTS) {
const sortedByAlpha = [...usersMatch, ...remoteChannels.matches].
sort(sortByUserOrChannel.bind(null, locale, teammateDisplayNameSetting));
items.push(...sortedByAlpha.slice(0, MAX_RESULTS + 1));
}
// Archived channels
if (items.length < MAX_RESULTS) {
const archivedAlpha = [...archivedChannels, ...remoteChannels.archived].
sort(sortChannelsByDisplayName.bind(null, locale));
items.push(...archivedAlpha.slice(0, MAX_RESULTS + 1));
}
return [...new Set(items)].slice(0, MAX_RESULTS + 1);
}, [archivedChannels, channelsMatchStart, channelsMatch, remoteChannels, usersMatch, usersMatchStart, locale, teammateDisplayNameSetting]);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
});
useEffect(() => {
bounce.current = debounce(search, 250);
bounce.current();
return () => {
if (bounce.current) {
bounce.current.cancel();
}
};
}, [term]);
return (
<Animated.View
@ -72,7 +295,7 @@ const UnfilteredList = ({close, keyboardHeight, showTeamName, term}: Props) => {
contentContainerStyle={flatListStyle}
keyboardDismissMode='interactive'
keyboardShouldPersistTaps='handled'
ListEmptyComponent={renderNoResults}
ListEmptyComponent={renderEmpty}
renderItem={renderItem}
data={data}
showsVerticalScrollIndicator={false}
@ -81,4 +304,4 @@ const UnfilteredList = ({close, keyboardHeight, showTeamName, term}: Props) => {
);
};
export default UnfilteredList;
export default FilteredList;

View file

@ -3,12 +3,17 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {General} from '@constants';
import {observeArchiveChannelsByTerm, observeDirectChannelsByTerm, observeJoinedChannelsByTerm, observeNotDirectChannelsByTerm} from '@queries/servers/channel';
import {observeConfig, observeCurrentTeamId} from '@queries/servers/system';
import {queryJoinedTeams} from '@queries/servers/team';
import {observeTeammateNameDisplay} from '@queries/servers/user';
import {retrieveChannels} from '@screens/find_channels/utils';
import FilteredList from './filtered_list';
import FilteredList, {MAX_RESULTS} from './filtered_list';
import type {WithDatabaseArgs} from '@typings/database/database';
@ -16,11 +21,50 @@ type EnhanceProps = WithDatabaseArgs & {
term: string;
}
const enhanced = withObservables([], ({database}: EnhanceProps) => {
const teamsCount = queryJoinedTeams(database).observeCount();
const enhanced = withObservables(['term'], ({database, term}: EnhanceProps) => {
const teamIds = queryJoinedTeams(database).observe().pipe(
// eslint-disable-next-line max-nested-callbacks
switchMap((teams) => of$(new Set(teams.map((t) => t.id)))),
);
const joinedChannelsMatchStart = observeJoinedChannelsByTerm(database, term, MAX_RESULTS, true);
const joinedChannelsMatch = observeJoinedChannelsByTerm(database, term, MAX_RESULTS);
const directChannelsMatchStart = observeDirectChannelsByTerm(database, term, MAX_RESULTS, true);
const directChannelsMatch = observeDirectChannelsByTerm(database, term, MAX_RESULTS);
const channelsMatchStart = combineLatest([joinedChannelsMatchStart, directChannelsMatchStart]).pipe(
switchMap((matchStart) => {
return retrieveChannels(database, matchStart.flat(), true);
}),
);
const channelsMatch = combineLatest([joinedChannelsMatch, directChannelsMatch]).pipe(
switchMap((matched) => retrieveChannels(database, matched.flat(), true)),
);
const archivedChannels = observeArchiveChannelsByTerm(database, term, MAX_RESULTS).pipe(
switchMap((archived) => retrieveChannels(database, archived)),
);
const usersMatchStart = observeNotDirectChannelsByTerm(database, term, MAX_RESULTS, true);
const usersMatch = observeNotDirectChannelsByTerm(database, term, MAX_RESULTS);
const restrictDirectMessage = observeConfig(database).pipe(
switchMap((cfg) => of$(cfg?.RestrictDirectMessage !== General.RESTRICT_DIRECT_MESSAGE_ANY)),
);
const teammateDisplayNameSetting = observeTeammateNameDisplay(database);
return {
showTeamName: teamsCount.pipe(switchMap((count) => of$(count > 1))),
archivedChannels,
channelsMatch,
channelsMatchStart,
currentTeamId: observeCurrentTeamId(database),
restrictDirectMessage,
showTeamName: teamIds.pipe(switchMap((ids) => of$(ids.size > 1))),
teamIds,
teammateDisplayNameSetting,
usersMatchStart,
usersMatch,
};
});

View file

@ -0,0 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import React from 'react';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeTeam} from '@queries/servers/team';
import RemoteChannelItem from './remote_channel_item';
import type {WithDatabaseArgs} from '@typings/database/database';
type EnhanceProps = WithDatabaseArgs & {
channel: Channel;
showTeamName?: boolean;
}
const enhance = withObservables(['channel', 'showTeamName'], ({channel, database, showTeamName}: EnhanceProps) => {
let teamDisplayName = of$('');
if (channel.team_id && showTeamName) {
teamDisplayName = observeTeam(database, channel.team_id).pipe(
switchMap((team) => of$(team?.displayName || '')),
);
}
return {
teamDisplayName,
};
});
export default React.memo(withDatabase(enhance(RemoteChannelItem)));

View file

@ -0,0 +1,128 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import ChannelIcon from '@components/channel_icon';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {
onPress: (channelId: string, displayName: string) => void;
channel: Channel;
teamDisplayName?: string;
testID?: string;
}
export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
flexDirection: 'row',
paddingHorizontal: 0,
minHeight: 44,
alignItems: 'center',
marginVertical: 2,
},
wrapper: {
flex: 1,
flexDirection: 'row',
},
text: {
marginTop: -1,
color: theme.centerChannelColor,
paddingLeft: 12,
paddingRight: 20,
...typography('Body', 200, 'Regular'),
},
teamName: {
color: changeOpacity(theme.centerChannelColor, 0.64),
paddingLeft: 12,
marginTop: 4,
...typography('Body', 75),
},
teamNameTablet: {
marginLeft: -12,
paddingLeft: 0,
marginTop: 0,
paddingBottom: 0,
top: 5,
},
}));
export const textStyle = StyleSheet.create({
bright: typography('Body', 200, 'SemiBold'),
regular: typography('Body', 200, 'Regular'),
});
const RemoteChannelItem = ({onPress, channel, teamDisplayName, testID}: Props) => {
const theme = useTheme();
const isTablet = useIsTablet();
const styles = getStyleSheet(theme);
const height = (teamDisplayName && !isTablet) ? 58 : 44;
const handleOnPress = useCallback(() => {
onPress(channel.id, channel.display_name);
}, [channel]);
const containerStyle = useMemo(() => [
styles.container,
{minHeight: height},
],
[height, styles]);
return (
<TouchableOpacity onPress={handleOnPress}>
<>
<View
style={containerStyle}
testID={`${testID}.${channel.name}`}
>
<View style={styles.wrapper}>
<ChannelIcon
isInfo={true}
isArchived={channel.delete_at > 0}
name={channel.name}
shared={channel.shared}
size={24}
type={channel.type}
/>
<View>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={styles.text}
testID={`${testID}.${channel.name}.display_name`}
>
{channel.display_name}
</Text>
{Boolean(teamDisplayName) && !isTablet &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
testID={`${testID}.${teamDisplayName}.display_name`}
style={styles.teamName}
>
{teamDisplayName}
</Text>
}
</View>
{Boolean(teamDisplayName) && isTablet &&
<Text
ellipsizeMode='tail'
numberOfLines={1}
testID={`${testID}.${teamDisplayName}.display_name`}
style={[styles.teamName, styles.teamNameTablet]}
>
{teamDisplayName}
</Text>
}
</View>
</View>
</>
</TouchableOpacity>
);
};
export default RemoteChannelItem;

View file

@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import React from 'react';
import {observeCurrentUserId} from '@queries/servers/system';
import {observeTeammateNameDisplay} from '@queries/servers/user';
import UserItem from './user_item';
import type {WithDatabaseArgs} from '@typings/database/database';
import type UserModel from '@typings/database/models/servers/user';
type EnhanceProps = WithDatabaseArgs & {
user: UserModel;
}
const enhance = withObservables(['user'], ({database, user}: EnhanceProps) => ({
currentUserId: observeCurrentUserId(database),
teammateDisplayNameSetting: observeTeammateNameDisplay(database),
user: user.observe(),
}));
export default React.memo(withDatabase(enhance(UserItem)));

View file

@ -0,0 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Text, TouchableOpacity, View} from 'react-native';
import CustomStatus from '@components/channel_item/custom_status';
import ProfilePicture from '@components/profile_picture';
import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername} from '@utils/user';
import type UserModel from '@typings/database/models/servers/user';
type Props = {
currentUserId: string;
onPress: (channelId: string, displayName: string) => void;
teammateDisplayNameSetting?: string;
testID?: string;
user: UserModel;
}
export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
flexDirection: 'row',
paddingHorizontal: 0,
height: 44,
alignItems: 'center',
marginVertical: 2,
},
wrapper: {
flex: 1,
flexDirection: 'row',
},
text: {
marginTop: -1,
color: theme.centerChannelColor,
paddingLeft: 12,
paddingRight: 20,
...typography('Body', 200, 'Regular'),
},
avatar: {marginLeft: 4},
status: {
backgroundColor: theme.centerChannelBg,
borderWidth: 0,
},
}));
const UserItem = ({currentUserId, onPress, teammateDisplayNameSetting, testID, user}: Props) => {
const {formatMessage, locale} = useIntl();
const theme = useTheme();
const styles = getStyleSheet(theme);
const isOwnDirectMessage = currentUserId === user.id;
const displayName = displayUsername(user, locale, teammateDisplayNameSetting);
const handleOnPress = useCallback(() => {
onPress(user.id, displayName);
}, [user.id, displayName, onPress]);
return (
<TouchableOpacity onPress={handleOnPress}>
<>
<View
style={styles.container}
testID={`${testID}.${user.id}`}
>
<View style={styles.wrapper}>
<View style={styles.avatar}>
<ProfilePicture
author={user}
size={24}
showStatus={true}
statusSize={12}
statusStyle={styles.status}
/>
</View>
<View>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={styles.text}
testID={`${testID}.${user.id}.display_name`}
>
{isOwnDirectMessage ? formatMessage({id: 'channel_header.directchannel.you', defaultMessage: '{displayName} (you)'}, {displayName}) : displayName}
</Text>
</View>
<CustomStatus userId={user.id}/>
</View>
</View>
</>
</TouchableOpacity>
);
};
export default UserItem;

View file

@ -43,6 +43,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const FindChannels = ({closeButtonId, componentId}: Props) => {
const theme = useTheme();
const [term, setTerm] = useState('');
const [loading, setLoading] = useState(false);
const styles = getStyleSheet(theme);
const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]);
const keyboardHeight = useKeyboardHeight();
@ -66,6 +67,9 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
const onChangeText = useCallback((text) => {
setTerm(text);
if (!text) {
setLoading(false);
}
}, []);
useEffect(() => {
@ -83,16 +87,17 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
<SearchBar
autoCapitalize='none'
autoFocus={true}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
clearIconColor={color}
searchIconColor={color}
cancelButtonProps={cancelButtonProps}
placeholderTextColor={color}
inputStyle={styles.inputStyle}
clearIconColor={color}
inputContainerStyle={styles.inputContainerStyle}
selectionColor={color}
inputStyle={styles.inputStyle}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onCancel={onCancel}
onChangeText={onChangeText}
placeholderTextColor={color}
searchIconColor={color}
selectionColor={color}
showLoading={loading}
value={term}
/>
{term === '' && <QuickOptions close={close}/>}
@ -107,6 +112,8 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
<FilteredList
close={close}
keyboardHeight={keyboardHeight}
loading={loading}
onLoading={setLoading}
term={term}
/>
}

View file

@ -9,26 +9,21 @@ import {switchMap} from 'rxjs/operators';
import {queryMyChannelsByUnread} from '@queries/servers/channel';
import {queryJoinedTeams} from '@queries/servers/team';
import {retrieveChannels} from '@screens/find_channels/utils';
import UnfilteredList from './unfiltered_list';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
const MAX_UNREAD_CHANNELS = 10;
const MAX_CHANNELS = 20;
const observeChannels = async (myChannels: MyChannelModel[]) => {
const channels = await Promise.all(myChannels.map((m) => m.channel.fetch()));
return channels.filter((c): c is ChannelModel => c !== null);
};
const observeRecentChannels = (database: Database, unreads: ChannelModel[]) => {
const count = MAX_CHANNELS - unreads.length;
const unreadIds = unreads.map((u) => u.id);
return queryMyChannelsByUnread(database, false, 'last_viewed_at', count, unreadIds).observe().pipe(
switchMap((myChannels) => observeChannels(myChannels)),
switchMap((myChannels) => retrieveChannels(database, myChannels, true)),
);
};
@ -37,7 +32,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const unreadChannels = queryMyChannelsByUnread(database, true, 'last_post_at', MAX_UNREAD_CHANNELS).
observeWithColumns(['last_post_at']).pipe(
switchMap((myChannels) => observeChannels(myChannels)),
switchMap((myChannels) => retrieveChannels(database, myChannels)),
);
const recentChannels = unreadChannels.pipe(

View file

@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import {of as of$} from 'rxjs';
import {MM_TABLES} from '@constants/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
const {SERVER: {CHANNEL, MY_CHANNEL}} = MM_TABLES;
export const retrieveChannels = (database: Database, myChannels: MyChannelModel[], orderedByLastViewedAt = false) => {
const ids = myChannels.map((m) => m.id);
if (ids.length) {
const idsStr = `'${ids.join("','")}'`;
const order = orderedByLastViewedAt ? 'order by my.last_viewed_at desc' : '';
return database.get<ChannelModel>(CHANNEL).query(
Q.unsafeSqlQuery(`select distinct c.* from ${MY_CHANNEL} my
inner join ${CHANNEL} c on c.id=my.id and c.id in (${idsStr})
${order}`),
).observe();
}
return of$([]);
};

View file

@ -117,7 +117,7 @@ const enhance = withObservables(['category', 'locale'], ({category, locale, data
const dmMap = (p: PreferenceModel) => getDirectChannelName(p.name, currentUserId);
const hiddenDmIds = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, undefined, 'false').
observe().pipe(
observeWithColumns(['value']).pipe(
switchMap((prefs: PreferenceModel[]) => {
const names = prefs.map(dmMap);
const channels = queryChannelsByNames(database, names).observe();
@ -129,15 +129,16 @@ const enhance = withObservables(['category', 'locale'], ({category, locale, data
);
const hiddenGmIds = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_GROUP_CHANNEL_SHOW, undefined, 'false').
observe().pipe(switchMap(mapPrefName));
observeWithColumns(['value']).pipe(switchMap(mapPrefName));
let limit = of$(Preferences.CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT);
if (category.type === DMS_CATEGORY) {
limit = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_LIMIT_DMS).observe().pipe(
switchMap((val) => {
return val[0] ? of$(parseInt(val[0].value, 10)) : of$(Preferences.CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT);
}),
);
limit = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SIDEBAR_SETTINGS, Preferences.CHANNEL_SIDEBAR_LIMIT_DMS).
observeWithColumns(['value']).pipe(
switchMap((val) => {
return val[0] ? of$(parseInt(val[0].value, 10)) : of$(Preferences.CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT);
}),
);
}
const hiddenChannelIds = combineLatest([hiddenDmIds, hiddenGmIds]).pipe(switchMap(

View file

@ -15,6 +15,7 @@ class EphemeralStore {
// of the extra computation time. We use this to track the events that are being handled
// and make sure we only handle one.
private addingTeam = new Set<string>();
private joiningChannels = new Set<string>();
addNavigationComponentId = (componentId: string) => {
this.addToNavigationComponentIdStack(componentId);
@ -121,6 +122,19 @@ class EphemeralStore {
}
};
// Ephemeral control when joining a channel locally
addJoiningChannel = (channelId: string) => {
this.joiningChannels.add(channelId);
};
isJoiningChannel = (channelId: string) => {
return this.joiningChannels.has(channelId);
};
removeJoiningChanel = (channelId: string) => {
this.joiningChannels.delete(channelId);
};
startAddingToTeam = (teamId: string) => {
this.addingTeam.add(teamId);
};

View file

@ -223,7 +223,6 @@
"global_threads.options.mark_as_read": "Mark as Read",
"global_threads.options.open_in_channel": "Open in Channel",
"global_threads.options.title": "THREAD ACTIONS",
"global_threads.options.unfollow": "Unfollow Thread",
"global_threads.unreads": "Unread Threads",
"home.header.plus_menu": "Options",
"interactive_dialog.submit": "Submit",
@ -311,6 +310,7 @@
"mobile.custom_status.clear_after": "Clear After",
"mobile.custom_status.clear_after.title": "Clear Custom Status After",
"mobile.custom_status.modal_confirm": "Done",
"mobile.direct_message.error": "We couldn't open a DM with {displayName}.",
"mobile.document_preview.failed_description": "An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n",
"mobile.document_preview.failed_title": "Open Document failed",
"mobile.downloader.disabled_description": "File downloads are disabled on this server. Please contact your System Admin for more details.\n",
@ -335,7 +335,7 @@
"mobile.gallery.title": "{index} of {total}",
"mobile.ios.photos_permission_denied_description": "Upload photos and videos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo and video library.",
"mobile.ios.photos_permission_denied_title": "{applicationName} would like to access your photos",
"mobile.join_channel.error": "We couldn't join the channel {displayName}. Please check your connection and try again.",
"mobile.join_channel.error": "We couldn't join the channel {displayName}.",
"mobile.link.error.text": "Unable to open the link.",
"mobile.link.error.title": "Error",
"mobile.login_options.cant_heading": "Can't Log In",
@ -567,6 +567,7 @@
"thread.header.thread_dm": "Direct Message Thread",
"thread.header.thread_in": "in {channelName}",
"thread.noReplies": "No replies yet",
"thread.options.title": "THREAD ACTIONS",
"thread.repliesCount": "{repliesCount, number} {repliesCount, plural, one {reply} other {replies}}",
"threads": "Threads",
"threads.deleted": "Original Message Deleted",