forked from Ivasoft/mattermost-mobile
Compare commits
12 Commits
test1.0.4
...
release-2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15fd6925b3 | ||
|
|
571070e284 | ||
|
|
ab8a43032e | ||
|
|
6904be23da | ||
|
|
6bc7c05ccb | ||
|
|
4b142483a5 | ||
|
|
63674e2a43 | ||
|
|
cdaf1f50e7 | ||
|
|
10735dcbf1 | ||
|
|
619decd253 | ||
|
|
55f18bcfc3 | ||
|
|
870336142a |
@@ -11,6 +11,7 @@ import com.mattermost.helpers.database_extension.queryCurrentUserId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean): Triple<ReadableMap?, ReadableMap?, ReadableArray?> {
|
||||
val channel = fetch(serverUrl, "/api/v4/channels/$channelId")
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {markChannelAsViewed} from '@actions/local/channel';
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
|
||||
@@ -440,9 +439,9 @@ export async function handleEntryAfterLoadNavigation(
|
||||
if (!currentTeamIdAfterLoad) {
|
||||
// First load or no team
|
||||
if (tabletDevice) {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
}
|
||||
} else if (currentTeamIdAfterLoad !== currentTeamId) {
|
||||
// Switched teams while loading
|
||||
@@ -466,9 +465,6 @@ export async function handleEntryAfterLoadNavigation(
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
}
|
||||
} else if (tabletDevice && initialChannelId === currentChannelId) {
|
||||
await markChannelAsRead(serverUrl, initialChannelId);
|
||||
markChannelAsViewed(serverUrl, initialChannelId);
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('could not manage the entry after load navigation', error);
|
||||
|
||||
@@ -130,6 +130,13 @@ async function doReconnect(serverUrl: string) {
|
||||
if (models?.length) {
|
||||
await operator.batchRecords(models, 'doReconnect');
|
||||
}
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
if (tabletDevice && initialChannelId === currentChannelId) {
|
||||
await markChannelAsRead(serverUrl, initialChannelId);
|
||||
markChannelAsViewed(serverUrl, initialChannelId);
|
||||
}
|
||||
|
||||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {Platform} from 'react-native';
|
||||
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {getConfigValue} from '@queries/servers/system';
|
||||
import {hasReliableWebsocket} from '@utils/config';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {logError, logInfo, logWarning} from '@utils/log';
|
||||
@@ -79,8 +79,12 @@ export default class WebSocketClient {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
const connectionUrl = (config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
|
||||
const [websocketUrl, version, reliableWebsocketConfig] = await Promise.all([
|
||||
getConfigValue(database, 'WebsocketURL'),
|
||||
getConfigValue(database, 'Version'),
|
||||
getConfigValue(database, 'EnableReliableWebSockets'),
|
||||
]);
|
||||
const connectionUrl = (websocketUrl || this.serverUrl) + '/api/v4/websocket';
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
@@ -101,7 +105,7 @@ export default class WebSocketClient {
|
||||
|
||||
this.url = connectionUrl;
|
||||
|
||||
const reliableWebSockets = hasReliableWebsocket(config);
|
||||
const reliableWebSockets = hasReliableWebsocket(version, reliableWebsocketConfig);
|
||||
if (reliableWebSockets) {
|
||||
// Add connection id, and last_sequence_number to the query param.
|
||||
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
|
||||
@@ -129,6 +133,11 @@ export default class WebSocketClient {
|
||||
headers.Authorization = `Bearer ${this.token}`;
|
||||
}
|
||||
const {client} = await getOrCreateWebSocketClient(this.url, {headers, timeoutInterval: WEBSOCKET_TIMEOUT});
|
||||
|
||||
// Check again if the client is the same, to avoid race conditions
|
||||
if (this.conn === client) {
|
||||
return;
|
||||
}
|
||||
this.conn = client;
|
||||
} catch (error) {
|
||||
return;
|
||||
|
||||
@@ -283,9 +283,9 @@ export default function PostInput({
|
||||
});
|
||||
return () => {
|
||||
listener.remove();
|
||||
updateDraftMessage(serverUrl, channelId, rootId, value); // safe draft on unmount
|
||||
updateDraftMessage(serverUrl, channelId, rootId, lastNativeValue.current); // safe draft on unmount
|
||||
};
|
||||
}, [updateValue, value, channelId, rootId]);
|
||||
}, [updateValue, channelId, rootId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== lastNativeValue.current) {
|
||||
|
||||
@@ -108,7 +108,7 @@ const CombinedUserActivity = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const passProps = {post};
|
||||
const passProps = {post, sourceScreen: location};
|
||||
Keyboard.dismiss();
|
||||
const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : '';
|
||||
|
||||
@@ -117,7 +117,7 @@ const CombinedUserActivity = ({
|
||||
} else {
|
||||
showModalOverCurrentContext(Screens.POST_OPTIONS, passProps, bottomSheetModalOptions(theme));
|
||||
}
|
||||
}, [post, canDelete, isTablet, intl]);
|
||||
}, [post, canDelete, isTablet, intl, location]);
|
||||
|
||||
const renderMessage = (postType: string, userIds: string[], actorId: string) => {
|
||||
let actor = '';
|
||||
|
||||
@@ -86,11 +86,11 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
if (reaction) {
|
||||
const emojiAlias = getEmojiFirstAlias(reaction.emojiName);
|
||||
if (acc.has(emojiAlias)) {
|
||||
const rs = acc.get(emojiAlias);
|
||||
const rs = acc.get(emojiAlias)!;
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const present = rs!.findIndex((r) => r.userId === reaction.userId) > -1;
|
||||
const present = rs.findIndex((r) => r.userId === reaction.userId) > -1;
|
||||
if (!present) {
|
||||
rs!.push(reaction);
|
||||
rs.push(reaction);
|
||||
}
|
||||
} else {
|
||||
acc.set(emojiAlias, [reaction]);
|
||||
@@ -105,7 +105,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
}, new Map<string, ReactionModel[]>());
|
||||
|
||||
return {reactionsByName, highlightedReactions};
|
||||
}, [sortedReactions]);
|
||||
}, [sortedReactions, reactions]);
|
||||
|
||||
const handleAddReactionToPost = (emoji: string) => {
|
||||
addReaction(serverUrl, postId, emoji);
|
||||
@@ -178,7 +178,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
return (
|
||||
<Reaction
|
||||
key={r}
|
||||
count={reaction!.length}
|
||||
count={reaction?.length || 1}
|
||||
emojiName={r}
|
||||
highlight={highlightedReactions.includes(r)}
|
||||
onPress={handleReactionPress}
|
||||
|
||||
@@ -150,6 +150,7 @@ export default function SelectedUsers({
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const numberSelectedIds = Object.keys(selectedIds).length;
|
||||
const bottomSpace = (dimensions.height - containerHeight - modalPosition);
|
||||
const bottomPaddingBottom = isTablet ? CHIP_HEIGHT_WITH_MARGIN : 0;
|
||||
|
||||
const users = useMemo(() => {
|
||||
const u = [];
|
||||
@@ -172,8 +173,8 @@ export default function SelectedUsers({
|
||||
}, [selectedIds, teammateNameDisplay, onRemove]);
|
||||
|
||||
const totalPanelHeight = useDerivedValue(() => (
|
||||
isVisible ? panelHeight.value + BUTTON_HEIGHT : 0
|
||||
), [isVisible, isTablet]);
|
||||
isVisible ? panelHeight.value + BUTTON_HEIGHT + bottomPaddingBottom : 0
|
||||
), [isVisible, isTablet, bottomPaddingBottom]);
|
||||
|
||||
const marginBottom = useMemo(() => {
|
||||
let margin = keyboard.height && Platform.OS === 'ios' ? keyboard.height - insets.bottom : 0;
|
||||
@@ -208,7 +209,7 @@ export default function SelectedUsers({
|
||||
}, [onPress]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
panelHeight.value = Math.min(PANEL_MAX_HEIGHT, e.nativeEvent.layout.height);
|
||||
panelHeight.value = Math.min(PANEL_MAX_HEIGHT + bottomPaddingBottom, e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const androidMaxHeight = Platform.select({
|
||||
@@ -235,8 +236,8 @@ export default function SelectedUsers({
|
||||
const animatedViewStyle = useAnimatedStyle(() => ({
|
||||
height: withTiming(totalPanelHeight.value + insets.bottom, {duration: 250}),
|
||||
borderWidth: isVisible ? 1 : 0,
|
||||
maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT + insets.bottom : 0,
|
||||
}), [isVisible, insets]);
|
||||
maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT + bottomPaddingBottom + insets.bottom : 0,
|
||||
}), [isVisible, insets, bottomPaddingBottom]);
|
||||
|
||||
const animatedButtonStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(isVisible ? 1 : 0, {duration: isVisible ? 500 : 100}),
|
||||
|
||||
@@ -14,7 +14,7 @@ import TeamList from './team_list';
|
||||
type Props = {
|
||||
iconPad?: boolean;
|
||||
canJoinOtherTeams: boolean;
|
||||
teamsCount: number;
|
||||
hasMoreThanOneTeam: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
@@ -36,8 +36,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default function TeamSidebar({iconPad, canJoinOtherTeams, teamsCount}: Props) {
|
||||
const initialWidth = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
export default function TeamSidebar({iconPad, canJoinOtherTeams, hasMoreThanOneTeam}: Props) {
|
||||
const initialWidth = hasMoreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
const width = useSharedValue(initialWidth);
|
||||
const marginTop = useSharedValue(iconPad ? 44 : 0);
|
||||
const theme = useTheme();
|
||||
@@ -58,8 +58,8 @@ export default function TeamSidebar({iconPad, canJoinOtherTeams, teamsCount}: Pr
|
||||
}, [iconPad]);
|
||||
|
||||
useEffect(() => {
|
||||
width.value = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
}, [teamsCount]);
|
||||
width.value = hasMoreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
}, [hasMoreThanOneTeam]);
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, transform]}>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
export const CATEGORIES_TO_KEEP: Record<string, string> = {
|
||||
ADVANCED_SETTINGS: 'advanced_settings',
|
||||
CHANNEL_APPROXIMATE_VIEW_TIME: 'channel_approximate_view_time',
|
||||
CHANNEL_OPEN_TIME: 'channel_open_time',
|
||||
DIRECT_CHANNEL_SHOW: 'direct_channel_show',
|
||||
GROUP_CHANNEL_SHOW: 'group_channel_show',
|
||||
DISPLAY_SETTINGS: 'display_settings',
|
||||
|
||||
@@ -246,10 +246,11 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
|
||||
const totalMsg = isCRT ? channel.total_msg_count_root! : channel.total_msg_count;
|
||||
const myMsgCount = isCRT ? my.msg_count_root! : my.msg_count;
|
||||
const msgCount = Math.max(0, totalMsg - myMsgCount);
|
||||
const lastPostAt = isCRT ? (channel.last_root_post_at || channel.last_post_at) : channel.last_post_at;
|
||||
my.msg_count = msgCount;
|
||||
my.mention_count = isCRT ? my.mention_count_root! : my.mention_count;
|
||||
my.is_unread = msgCount > 0;
|
||||
my.last_post_at = (isCRT ? (channel.last_root_post_at || channel.last_post_at) : channel.last_post_at) || 0;
|
||||
my.last_post_at = lastPostAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +272,7 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
|
||||
}
|
||||
|
||||
const chan = channelMap[my.channel_id];
|
||||
const lastPostAt = (isCRT ? chan.last_root_post_at : chan.last_post_at) || 0;
|
||||
const lastPostAt = isCRT ? (chan.last_root_post_at || chan.last_post_at) : chan.last_post_at;
|
||||
if ((chan && e.lastPostAt < lastPostAt) ||
|
||||
e.isUnread !== my.is_unread || e.lastViewedAt < my.last_viewed_at ||
|
||||
e.roles !== my.roles
|
||||
|
||||
@@ -37,22 +37,22 @@ class PushNotifications {
|
||||
configured = false;
|
||||
|
||||
init(register: boolean) {
|
||||
if (register) {
|
||||
this.registerIfNeeded();
|
||||
}
|
||||
|
||||
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
||||
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
||||
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
||||
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
||||
|
||||
if (register) {
|
||||
this.registerIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
async registerIfNeeded() {
|
||||
const isRegistered = await Notifications.isRegisteredForRemoteNotifications();
|
||||
if (!isRegistered) {
|
||||
await requestNotifications(['alert', 'sound', 'badge']);
|
||||
Notifications.registerRemoteNotifications();
|
||||
}
|
||||
Notifications.registerRemoteNotifications();
|
||||
}
|
||||
|
||||
createReplyCategory = () => {
|
||||
|
||||
@@ -11,14 +11,11 @@ import {makeCategoryChannelId} from '@utils/categories';
|
||||
import {pluckUnique} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import {observeChannelsByLastPostAt} from './channel';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type CategoryModel from '@typings/database/models/servers/category';
|
||||
import type CategoryChannelModel from '@typings/database/models/servers/category_channel';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
const {SERVER: {CATEGORY, CATEGORY_CHANNEL, CHANNEL}} = MM_TABLES;
|
||||
const {SERVER: {CATEGORY, CATEGORY_CHANNEL}} = MM_TABLES;
|
||||
|
||||
export const getCategoryById = async (database: Database, categoryId: string) => {
|
||||
try {
|
||||
@@ -144,24 +141,3 @@ export const observeIsChannelFavorited = (database: Database, teamId: string, ch
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeChannelsByCategoryChannelSortOrder = (database: Database, category: CategoryModel, excludeIds?: string[]) => {
|
||||
return category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']).pipe(
|
||||
switchMap((categoryChannels) => {
|
||||
const ids = categoryChannels.map((cc) => cc.channelId);
|
||||
const idsStr = `'${ids.join("','")}'`;
|
||||
const exclude = excludeIds?.length ? `AND c.id NOT IN ('${excludeIds.join("','")}')` : '';
|
||||
return database.get<ChannelModel>(CHANNEL).query(
|
||||
Q.unsafeSqlQuery(`SELECT DISTINCT c.* FROM ${CHANNEL} c INNER JOIN
|
||||
${CATEGORY_CHANNEL} cc ON cc.channel_id=c.id AND c.id IN (${idsStr}) ${exclude}
|
||||
ORDER BY cc.sort_order`),
|
||||
).observe();
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeChannelsByLastPostAtInCategory = (database: Database, category: CategoryModel, excludeIds?: string[]) => {
|
||||
return category.myChannels.observeWithColumns(['last_post_at']).pipe(
|
||||
switchMap((myChannels) => observeChannelsByLastPostAt(database, myChannels, excludeIds)),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {General, Permissions} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {sanitizeLikeString} from '@helpers/database';
|
||||
import {hasPermission} from '@utils/role';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import {prepareDeletePost} from './post';
|
||||
import {queryRoles} from './role';
|
||||
@@ -437,10 +438,6 @@ export const observeNotifyPropsByChannels = (database: Database, channels: Chann
|
||||
);
|
||||
};
|
||||
|
||||
export const queryChannelsByNames = (database: Database, names: string[]) => {
|
||||
return database.get<ChannelModel>(CHANNEL).query(Q.where('name', Q.oneOf(names)));
|
||||
};
|
||||
|
||||
export const queryMyChannelUnreads = (database: Database, currentTeamId: string) => {
|
||||
return database.get<MyChannelModel>(MY_CHANNEL).query(
|
||||
Q.on(
|
||||
@@ -453,40 +450,42 @@ export const queryMyChannelUnreads = (database: Database, currentTeamId: string)
|
||||
Q.where('delete_at', Q.eq(0)),
|
||||
),
|
||||
),
|
||||
Q.where('is_unread', Q.eq(true)),
|
||||
Q.or(
|
||||
Q.where('is_unread', Q.eq(true)),
|
||||
Q.where('mentions_count', Q.gte(0)),
|
||||
),
|
||||
Q.sortBy('last_post_at', Q.desc),
|
||||
);
|
||||
};
|
||||
|
||||
export const queryEmptyDirectAndGroupChannels = (database: Database) => {
|
||||
return database.get<MyChannelModel>(MY_CHANNEL).query(
|
||||
Q.on(
|
||||
CHANNEL,
|
||||
Q.where('team_id', Q.eq('')),
|
||||
),
|
||||
Q.where('last_post_at', Q.eq(0)),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeArchivedDirectChannels = (database: Database, currentUserId: string) => {
|
||||
const deactivatedIds = database.get<UserModel>(USER).query(
|
||||
const deactivated = database.get<UserModel>(USER).query(
|
||||
Q.where('delete_at', Q.gt(0)),
|
||||
).observe().pipe(
|
||||
switchMap((users) => of$(users.map((u) => u.id))),
|
||||
);
|
||||
).observe();
|
||||
|
||||
return deactivatedIds.pipe(
|
||||
switchMap((dIds) => {
|
||||
return deactivated.pipe(
|
||||
switchMap((users) => {
|
||||
const usersMap = new Map(users.map((u) => [u.id, u]));
|
||||
return database.get<ChannelModel>(CHANNEL).query(
|
||||
Q.on(
|
||||
CHANNEL_MEMBERSHIP,
|
||||
Q.and(
|
||||
Q.where('user_id', Q.notEq(currentUserId)),
|
||||
Q.where('user_id', Q.oneOf(dIds)),
|
||||
Q.where('user_id', Q.oneOf(Array.from(usersMap.keys()))),
|
||||
),
|
||||
),
|
||||
Q.where('type', 'D'),
|
||||
).observe();
|
||||
).observe().pipe(
|
||||
switchMap((channels) => {
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
return of$(new Map(channels.map((c) => {
|
||||
const teammateId = getUserIdFromChannelName(currentUserId, c.name);
|
||||
const user = usersMap.get(teammateId);
|
||||
|
||||
return [c.id, user];
|
||||
})));
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -639,13 +638,13 @@ export const observeIsMutedSetting = (database: Database, channelId: string) =>
|
||||
return observeChannelSettings(database, channelId).pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === General.MENTION)));
|
||||
};
|
||||
|
||||
export const observeChannelsByLastPostAt = (database: Database, myChannels: MyChannelModel[], excludeIds?: string[]) => {
|
||||
export const observeChannelsByLastPostAt = (database: Database, myChannels: MyChannelModel[]) => {
|
||||
const ids = myChannels.map((c) => c.id);
|
||||
const idsStr = `'${ids.join("','")}'`;
|
||||
const exclude = excludeIds?.length ? `AND c.id NOT IN ('${excludeIds.join("','")}')` : '';
|
||||
|
||||
return database.get<ChannelModel>(CHANNEL).query(
|
||||
Q.unsafeSqlQuery(`SELECT DISTINCT c.* FROM ${CHANNEL} c INNER JOIN
|
||||
${MY_CHANNEL} mc ON mc.id=c.id AND c.id IN (${idsStr}) ${exclude}
|
||||
${MY_CHANNEL} mc ON mc.id=c.id AND c.id IN (${idsStr})
|
||||
ORDER BY CASE mc.last_post_at WHEN 0 THEN c.create_at ELSE mc.last_post_at END DESC`),
|
||||
).observe();
|
||||
};
|
||||
|
||||
@@ -197,8 +197,8 @@ export default function CreateDirectMessage({
|
||||
setSelectedIds((current) => removeProfileFromList(current, id));
|
||||
}, []);
|
||||
|
||||
const createDirectChannel = useCallback(async (id: string): Promise<boolean> => {
|
||||
const user = selectedIds[id];
|
||||
const createDirectChannel = useCallback(async (id: string, selectedUser?: UserProfile): Promise<boolean> => {
|
||||
const user = selectedUser || selectedIds[id];
|
||||
const displayName = displayUsername(user, intl.locale, teammateNameDisplay);
|
||||
const result = await makeDirectChannel(serverUrl, id, displayName);
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function CreateDirectMessage({
|
||||
return !result.error;
|
||||
}, [serverUrl]);
|
||||
|
||||
const startConversation = useCallback(async (selectedId?: {[id: string]: boolean}) => {
|
||||
const startConversation = useCallback(async (selectedId?: {[id: string]: boolean}, selectedUser?: UserProfile) => {
|
||||
if (startingConversation) {
|
||||
return;
|
||||
}
|
||||
@@ -233,7 +233,7 @@ export default function CreateDirectMessage({
|
||||
} else if (idsToUse.length > 1) {
|
||||
success = await createGroupChannel(idsToUse);
|
||||
} else {
|
||||
success = await createDirectChannel(idsToUse[0]);
|
||||
success = await createDirectChannel(idsToUse[0], selectedUser);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
@@ -249,7 +249,7 @@ export default function CreateDirectMessage({
|
||||
[currentUserId]: true,
|
||||
};
|
||||
|
||||
startConversation(selectedId);
|
||||
startConversation(selectedId, user);
|
||||
} else {
|
||||
clearSearch();
|
||||
setSelectedIds((current) => {
|
||||
|
||||
@@ -102,6 +102,7 @@ const EditProfile = ({
|
||||
popTopScreen(componentId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const enableSaveButton = useCallback((value: boolean) => {
|
||||
if (!isTablet) {
|
||||
const buttons = {
|
||||
@@ -114,18 +115,19 @@ const EditProfile = ({
|
||||
}
|
||||
setCanSave(value);
|
||||
}, [componentId, rightButton]);
|
||||
|
||||
const submitUser = useCallback(preventDoubleTap(async () => {
|
||||
enableSaveButton(false);
|
||||
setError(undefined);
|
||||
setUpdating(true);
|
||||
try {
|
||||
const newUserInfo: Partial<UserProfile> = {
|
||||
email: userInfo.email,
|
||||
first_name: userInfo.firstName,
|
||||
last_name: userInfo.lastName,
|
||||
nickname: userInfo.nickname,
|
||||
position: userInfo.position,
|
||||
username: userInfo.username,
|
||||
email: userInfo.email.trim(),
|
||||
first_name: userInfo.firstName.trim(),
|
||||
last_name: userInfo.lastName.trim(),
|
||||
nickname: userInfo.nickname.trim(),
|
||||
position: userInfo.position.trim(),
|
||||
username: userInfo.username.trim(),
|
||||
};
|
||||
const localPath = changedProfilePicture.current?.localPath;
|
||||
const profileImageRemoved = changedProfilePicture.current?.isRemoved;
|
||||
|
||||
@@ -12,29 +12,9 @@ import TestHelper from '@test/test_helper';
|
||||
import CategoryBody from '.';
|
||||
|
||||
import type CategoryModel from '@typings/database/models/servers/category';
|
||||
import type CategoryChannelModel from '@typings/database/models/servers/category_channel';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
const {SERVER: {CATEGORY}} = MM_TABLES;
|
||||
|
||||
jest.mock('@queries/servers/categories', () => {
|
||||
const Queries = jest.requireActual('@queries/servers/categories');
|
||||
const switchMap = jest.requireActual('rxjs/operators').switchMap;
|
||||
const mQ = jest.requireActual('@nozbe/watermelondb').Q;
|
||||
|
||||
return {
|
||||
...Queries,
|
||||
observeChannelsByCategoryChannelSortOrder: (database: Database, category: CategoryModel, excludeIds?: string[]) => {
|
||||
return category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']).pipe(
|
||||
switchMap((categoryChannels: CategoryChannelModel[]) => {
|
||||
const ids = categoryChannels.filter((cc) => excludeIds?.includes(cc.channelId)).map((cc) => cc.channelId);
|
||||
return database.get<ChannelModel>('Channel').query(mQ.where('id', mQ.oneOf(ids))).observe();
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('components/channel_list/categories/body', () => {
|
||||
let database: Database;
|
||||
let category: CategoryModel;
|
||||
|
||||
@@ -7,7 +7,6 @@ import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 're
|
||||
|
||||
import {fetchDirectChannelsInfo} from '@actions/remote/channel';
|
||||
import ChannelItem from '@components/channel_item';
|
||||
import {DMS_CATEGORY} from '@constants/categories';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
|
||||
@@ -17,7 +16,6 @@ import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
type Props = {
|
||||
sortedChannels: ChannelModel[];
|
||||
category: CategoryModel;
|
||||
limit: number;
|
||||
onChannelSwitch: (channelId: string) => void;
|
||||
unreadIds: Set<string>;
|
||||
unreadsOnTop: boolean;
|
||||
@@ -25,16 +23,13 @@ type Props = {
|
||||
|
||||
const extractKey = (item: ChannelModel) => item.id;
|
||||
|
||||
const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, limit, onChannelSwitch}: Props) => {
|
||||
const CategoryBody = ({sortedChannels, unreadIds, unreadsOnTop, category, onChannelSwitch}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const ids = useMemo(() => {
|
||||
const filteredChannels = unreadsOnTop ? sortedChannels.filter((c) => !unreadIds.has(c.id)) : sortedChannels;
|
||||
|
||||
if (category.type === DMS_CATEGORY && limit > 0) {
|
||||
return filteredChannels.slice(0, limit);
|
||||
}
|
||||
return filteredChannels;
|
||||
}, [category.type, limit, sortedChannels, unreadIds, unreadsOnTop]);
|
||||
}, [category.type, sortedChannels, unreadIds, unreadsOnTop]);
|
||||
|
||||
const unreadChannels = useMemo(() => {
|
||||
return unreadsOnTop ? [] : ids.filter((c) => unreadIds.has(c.id));
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {map, switchMap, combineLatestWith} from 'rxjs/operators';
|
||||
import {of as of$, Observable} from 'rxjs';
|
||||
import {switchMap, combineLatestWith, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {General, Preferences} from '@constants';
|
||||
import {Preferences} from '@constants';
|
||||
import {DMS_CATEGORY} from '@constants/categories';
|
||||
import {getSidebarPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {observeChannelsByCategoryChannelSortOrder, observeChannelsByLastPostAtInCategory} from '@queries/servers/categories';
|
||||
import {observeArchivedDirectChannels, observeNotifyPropsByChannels, queryChannelsByNames, queryEmptyDirectAndGroupChannels} from '@queries/servers/channel';
|
||||
import {observeArchivedDirectChannels, observeNotifyPropsByChannels} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName, querySidebarPreferences} from '@queries/servers/preference';
|
||||
import {observeCurrentChannelId, observeCurrentUserId, observeLastUnreadChannelId} from '@queries/servers/system';
|
||||
import {getDirectChannelName} from '@utils/channel';
|
||||
import {ChannelWithMyChannel, filterArchivedChannels, filterAutoclosedDMs, filterManuallyClosedDms, getUnreadIds, sortChannels} from '@utils/categories';
|
||||
|
||||
import CategoryBody from './category_body';
|
||||
|
||||
@@ -24,10 +22,6 @@ import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
|
||||
type ChannelData = Pick<ChannelModel, 'id' | 'displayName'> & {
|
||||
isMuted: boolean;
|
||||
};
|
||||
|
||||
type EnhanceProps = {
|
||||
category: CategoryModel;
|
||||
locale: string;
|
||||
@@ -35,87 +29,45 @@ type EnhanceProps = {
|
||||
isTablet: boolean;
|
||||
} & WithDatabaseArgs
|
||||
|
||||
const sortAlpha = (locale: string, a: ChannelData, b: ChannelData) => {
|
||||
if (a.isMuted && !b.isMuted) {
|
||||
return 1;
|
||||
} else if (!a.isMuted && b.isMuted) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return a.displayName.localeCompare(b.displayName, locale, {numeric: true});
|
||||
};
|
||||
|
||||
const filterArchived = (channels: Array<ChannelModel | null>, currentChannelId: string) => {
|
||||
return channels.filter((c): c is ChannelModel => c != null && ((c.deleteAt > 0 && c.id === currentChannelId) || !c.deleteAt));
|
||||
};
|
||||
|
||||
const buildAlphaData = (channels: ChannelModel[], notifyProps: Record<string, Partial<ChannelNotifyProps>>, locale: string) => {
|
||||
const chanelsById = channels.reduce((result: Record<string, ChannelModel>, c) => {
|
||||
result[c.id] = c;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const combined = channels.map((c) => {
|
||||
const s = notifyProps[c.id];
|
||||
return {
|
||||
id: c.id,
|
||||
displayName: c.displayName,
|
||||
isMuted: s?.mark_unread === General.MENTION,
|
||||
};
|
||||
});
|
||||
|
||||
combined.sort(sortAlpha.bind(null, locale));
|
||||
return of$(combined.map((cdata) => chanelsById[cdata.id]));
|
||||
};
|
||||
|
||||
const observeSortedChannels = (database: Database, category: CategoryModel, excludeIds: string[], locale: string) => {
|
||||
switch (category.sorting) {
|
||||
case 'alpha': {
|
||||
const channels = category.channels.extend(Q.where('id', Q.notIn(excludeIds))).observeWithColumns(['display_name']);
|
||||
const notifyProps = channels.pipe(switchMap((cs) => observeNotifyPropsByChannels(database, cs)));
|
||||
return combineLatest([channels, notifyProps]).pipe(
|
||||
switchMap(([cs, np]) => buildAlphaData(cs, np, locale)),
|
||||
);
|
||||
}
|
||||
case 'manual': {
|
||||
return observeChannelsByCategoryChannelSortOrder(database, category, excludeIds);
|
||||
}
|
||||
default:
|
||||
return observeChannelsByLastPostAtInCategory(database, category, excludeIds);
|
||||
}
|
||||
};
|
||||
|
||||
const mapPrefName = (prefs: PreferenceModel[]) => of$(prefs.map((p) => p.name));
|
||||
|
||||
const mapChannelIds = (channels: ChannelModel[] | MyChannelModel[]) => of$(channels.map((c) => c.id));
|
||||
|
||||
const withUserId = withObservables([], ({database}: WithDatabaseArgs) => ({currentUserId: observeCurrentUserId(database)}));
|
||||
|
||||
const enhance = withObservables(['category', 'isTablet', 'locale'], ({category, locale, isTablet, database, currentUserId}: EnhanceProps) => {
|
||||
const dmMap = (p: PreferenceModel) => getDirectChannelName(p.name, currentUserId);
|
||||
const observeCategoryChannels = (category: CategoryModel, myChannels: Observable<MyChannelModel[]>) => {
|
||||
const channels = category.channels.observeWithColumns(['create_at', 'display_name']);
|
||||
const manualSort = category.categoryChannelsBySortOrder.observeWithColumns(['sort_order']);
|
||||
return myChannels.pipe(
|
||||
combineLatestWith(channels, manualSort),
|
||||
switchMap(([my, cs, sorted]) => {
|
||||
const channelMap = new Map<string, ChannelModel>(cs.map((c) => [c.id, c]));
|
||||
const categoryChannelMap = new Map<string, number>(sorted.map((s) => [s.channelId, s.sortOrder]));
|
||||
return of$(my.reduce<ChannelWithMyChannel[]>((result, myChannel) => {
|
||||
const channel = channelMap.get(myChannel.id);
|
||||
if (channel) {
|
||||
const channelWithMyChannel: ChannelWithMyChannel = {
|
||||
channel,
|
||||
myChannel,
|
||||
sortOrder: categoryChannelMap.get(myChannel.id) || 0,
|
||||
};
|
||||
result.push(channelWithMyChannel);
|
||||
}
|
||||
|
||||
const currentChannelId = observeCurrentChannelId(database);
|
||||
return result;
|
||||
}, []));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const hiddenDmIds = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, undefined, 'false').
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap((prefs: PreferenceModel[]) => {
|
||||
const names = prefs.map(dmMap);
|
||||
const channels = queryChannelsByNames(database, names).observe();
|
||||
const enhanced = withObservables([], ({category, currentUserId, database, isTablet, locale}: EnhanceProps) => {
|
||||
const categoryMyChannels = category.myChannels.observeWithColumns(['last_post_at', 'is_unread']);
|
||||
const channelsWithMyChannel = observeCategoryChannels(category, categoryMyChannels);
|
||||
const currentChannelId = isTablet ? observeCurrentChannelId(database) : of$('');
|
||||
const lastUnreadId = isTablet ? observeLastUnreadChannelId(database) : of$(undefined);
|
||||
|
||||
return channels.pipe(
|
||||
switchMap(mapChannelIds),
|
||||
);
|
||||
}),
|
||||
const unreadsOnTop = querySidebarPreferences(database, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
|
||||
observeWithColumns(['value']).
|
||||
pipe(
|
||||
switchMap((prefs: PreferenceModel[]) => of$(getSidebarPreferenceAsBool(prefs, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS))),
|
||||
);
|
||||
|
||||
const emptyDmIds = queryEmptyDirectAndGroupChannels(database).observeWithColumns(['last_post_at']).pipe(
|
||||
switchMap(mapChannelIds),
|
||||
);
|
||||
|
||||
const archivedDmIds = observeArchivedDirectChannels(database, currentUserId).pipe(
|
||||
switchMap(mapChannelIds),
|
||||
);
|
||||
|
||||
let limit = of$(Preferences.CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT);
|
||||
if (category.type === DMS_CATEGORY) {
|
||||
limit = querySidebarPreferences(database, Preferences.CHANNEL_SIDEBAR_LIMIT_DMS).
|
||||
@@ -126,54 +78,61 @@ const enhance = withObservables(['category', 'isTablet', 'locale'], ({category,
|
||||
);
|
||||
}
|
||||
|
||||
const unreadsOnTop = querySidebarPreferences(database, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
|
||||
observeWithColumns(['value']).
|
||||
pipe(
|
||||
switchMap((prefs: PreferenceModel[]) => of$(getSidebarPreferenceAsBool(prefs, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS))),
|
||||
);
|
||||
|
||||
const lastUnreadId = isTablet ? observeLastUnreadChannelId(database) : of$(undefined);
|
||||
|
||||
const hiddenChannelIds = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, undefined, 'false').
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap(mapPrefName),
|
||||
combineLatestWith(hiddenDmIds, emptyDmIds, archivedDmIds, lastUnreadId),
|
||||
switchMap(([hIds, hDmIds, eDmIds, aDmIds, excludeId]) => {
|
||||
const hidden = new Set(hIds.concat(hDmIds, eDmIds, aDmIds));
|
||||
if (excludeId) {
|
||||
hidden.delete(excludeId);
|
||||
}
|
||||
return of$(hidden);
|
||||
}),
|
||||
);
|
||||
const sortedChannels = hiddenChannelIds.pipe(
|
||||
switchMap((excludeIds) => observeSortedChannels(database, category, Array.from(excludeIds), locale)),
|
||||
combineLatestWith(currentChannelId),
|
||||
map(([channels, ccId]) => filterArchived(channels, ccId)),
|
||||
const notifyPropsPerChannel = categoryMyChannels.pipe(
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
switchMap((mc) => observeNotifyPropsByChannels(database, mc)),
|
||||
);
|
||||
|
||||
const unreadChannels = category.myChannels.observeWithColumns(['mentions_count', 'is_unread']);
|
||||
const notifyProps = unreadChannels.pipe(switchMap((myChannels) => observeNotifyPropsByChannels(database, myChannels)));
|
||||
const unreadIds = unreadChannels.pipe(
|
||||
combineLatestWith(notifyProps, lastUnreadId),
|
||||
map(([my, settings, lastUnread]) => {
|
||||
return my.reduce<Set<string>>((set, m) => {
|
||||
const isMuted = settings[m.id]?.mark_unread === 'mention';
|
||||
if ((isMuted && m.mentionsCount) || (!isMuted && m.isUnread) || m.id === lastUnread) {
|
||||
set.add(m.id);
|
||||
}
|
||||
return set;
|
||||
}, new Set());
|
||||
const hiddenDmPrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, undefined, 'false').
|
||||
observeWithColumns(['value']);
|
||||
const hiddenGmPrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, undefined, 'false').
|
||||
observeWithColumns(['value']);
|
||||
const manuallyClosedPrefs = hiddenDmPrefs.pipe(
|
||||
combineLatestWith(hiddenGmPrefs),
|
||||
switchMap(([dms, gms]) => of$(dms.concat(gms))),
|
||||
);
|
||||
|
||||
const approxViewTimePrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.CHANNEL_APPROXIMATE_VIEW_TIME, undefined).
|
||||
observeWithColumns(['value']);
|
||||
const openTimePrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.CHANNEL_OPEN_TIME, undefined).
|
||||
observeWithColumns(['value']);
|
||||
const autoclosePrefs = approxViewTimePrefs.pipe(
|
||||
combineLatestWith(openTimePrefs),
|
||||
switchMap(([viewTimes, openTimes]) => of$(viewTimes.concat(openTimes))),
|
||||
);
|
||||
|
||||
const categorySorting = category.observe().pipe(
|
||||
switchMap((c) => of$(c.sorting)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const deactivated = (category.type === DMS_CATEGORY) ? observeArchivedDirectChannels(database, currentUserId) : of$(undefined);
|
||||
const sortedChannels = channelsWithMyChannel.pipe(
|
||||
combineLatestWith(categorySorting, currentChannelId, lastUnreadId, notifyPropsPerChannel, manuallyClosedPrefs, autoclosePrefs, deactivated, limit),
|
||||
switchMap(([cwms, sorting, channelId, unreadId, notifyProps, manuallyClosedDms, autoclose, deactivatedDMS, maxDms]) => {
|
||||
let channelsW = cwms;
|
||||
|
||||
channelsW = filterArchivedChannels(channelsW, channelId);
|
||||
channelsW = filterManuallyClosedDms(channelsW, notifyProps, manuallyClosedDms, currentUserId, unreadId);
|
||||
channelsW = filterAutoclosedDMs(category.type, maxDms, channelId, channelsW, autoclose, notifyProps, deactivatedDMS, unreadId);
|
||||
|
||||
return of$(sortChannels(sorting, channelsW, notifyProps, locale));
|
||||
}),
|
||||
);
|
||||
|
||||
const unreadIds = channelsWithMyChannel.pipe(
|
||||
combineLatestWith(notifyPropsPerChannel, lastUnreadId),
|
||||
switchMap(([cwms, notifyProps, unreadId]) => {
|
||||
return of$(getUnreadIds(cwms, notifyProps, unreadId));
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
limit,
|
||||
sortedChannels,
|
||||
unreadsOnTop,
|
||||
unreadIds,
|
||||
category,
|
||||
sortedChannels,
|
||||
unreadIds,
|
||||
unreadsOnTop,
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withUserId(enhance(CategoryBody)));
|
||||
export default withDatabase(withUserId(enhanced(CategoryBody)));
|
||||
|
||||
@@ -54,7 +54,7 @@ const enhanced = withObservables(['currentTeamId', 'isTablet', 'onlyUnreads'], (
|
||||
const channels = myUnreadChannels.pipe(switchMap((myChannels) => observeChannelsByLastPostAt(database, myChannels)));
|
||||
const channelsMap = channels.pipe(switchMap((cs) => of$(makeChannelsMap(cs))));
|
||||
|
||||
return queryMyChannelUnreads(database, currentTeamId).observeWithColumns(['last_post_at', 'is_unread']).pipe(
|
||||
return myUnreadChannels.pipe(
|
||||
combineLatestWith(channelsMap, notifyProps),
|
||||
map(filterAndSortMyChannels),
|
||||
combineLatestWith(lastUnread),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Permissions} from '@constants';
|
||||
import {observePermissionForTeam} from '@queries/servers/role';
|
||||
@@ -25,6 +25,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
|
||||
const canJoinChannels = combineLatest([currentUser, team]).pipe(
|
||||
switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.JOIN_PUBLIC_CHANNELS, true)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const canCreatePublicChannels = combineLatest([currentUser, team]).pipe(
|
||||
@@ -37,6 +38,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
|
||||
const canCreateChannels = combineLatest([canCreatePublicChannels, canCreatePrivateChannels]).pipe(
|
||||
switchMap(([open, priv]) => of$(open || priv)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const canAddUserToTeam = combineLatest([currentUser, team]).pipe(
|
||||
@@ -48,9 +50,11 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
canJoinChannels,
|
||||
canInvitePeople: combineLatest([enableOpenServer, canAddUserToTeam]).pipe(
|
||||
switchMap(([openServer, addUser]) => of$(openServer && addUser)),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
displayName: team.pipe(
|
||||
switchMap((t) => of$(t?.displayName)),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
pushProxyStatus: observePushVerificationStatus(database),
|
||||
};
|
||||
|
||||
@@ -33,8 +33,8 @@ describe('components/categories_list', () => {
|
||||
it('should render', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<CategoriesList
|
||||
teamsCount={1}
|
||||
channelsCount={1}
|
||||
moreThanOneTeam={false}
|
||||
hasChannels={true}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
@@ -46,8 +46,8 @@ describe('components/categories_list', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<CategoriesList
|
||||
isCRTEnabled={true}
|
||||
teamsCount={1}
|
||||
channelsCount={1}
|
||||
moreThanOneTeam={false}
|
||||
hasChannels={true}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
@@ -67,8 +67,8 @@ describe('components/categories_list', () => {
|
||||
jest.useFakeTimers();
|
||||
const wrapper = renderWithEverything(
|
||||
<CategoriesList
|
||||
teamsCount={0}
|
||||
channelsCount={1}
|
||||
moreThanOneTeam={false}
|
||||
hasChannels={true}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
@@ -89,8 +89,8 @@ describe('components/categories_list', () => {
|
||||
jest.useFakeTimers();
|
||||
const wrapper = renderWithEverything(
|
||||
<CategoriesList
|
||||
teamsCount={1}
|
||||
channelsCount={0}
|
||||
moreThanOneTeam={true}
|
||||
hasChannels={false}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
@@ -27,28 +27,28 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
}));
|
||||
|
||||
type ChannelListProps = {
|
||||
channelsCount: number;
|
||||
hasChannels: boolean;
|
||||
iconPad?: boolean;
|
||||
isCRTEnabled?: boolean;
|
||||
teamsCount: number;
|
||||
moreThanOneTeam: boolean;
|
||||
};
|
||||
|
||||
const getTabletWidth = (teamsCount: number) => {
|
||||
return TABLET_SIDEBAR_WIDTH - (teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0);
|
||||
const getTabletWidth = (moreThanOneTeam: boolean) => {
|
||||
return TABLET_SIDEBAR_WIDTH - (moreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0);
|
||||
};
|
||||
|
||||
const CategoriesList = ({channelsCount, iconPad, isCRTEnabled, teamsCount}: ChannelListProps) => {
|
||||
const CategoriesList = ({hasChannels, iconPad, isCRTEnabled, moreThanOneTeam}: ChannelListProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const {width} = useWindowDimensions();
|
||||
const isTablet = useIsTablet();
|
||||
const tabletWidth = useSharedValue(isTablet ? getTabletWidth(teamsCount) : 0);
|
||||
const tabletWidth = useSharedValue(isTablet ? getTabletWidth(moreThanOneTeam) : 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTablet) {
|
||||
tabletWidth.value = getTabletWidth(teamsCount);
|
||||
tabletWidth.value = getTabletWidth(moreThanOneTeam);
|
||||
}
|
||||
}, [isTablet && teamsCount]);
|
||||
}, [isTablet && moreThanOneTeam]);
|
||||
|
||||
const tabletStyle = useAnimatedStyle(() => {
|
||||
if (!isTablet) {
|
||||
@@ -61,7 +61,7 @@ const CategoriesList = ({channelsCount, iconPad, isCRTEnabled, teamsCount}: Chan
|
||||
}, [isTablet, width]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (channelsCount < 1) {
|
||||
if (!hasChannels) {
|
||||
return (<LoadChannelsError/>);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,10 @@ import Servers from './servers';
|
||||
import type {LaunchType} from '@typings/launch';
|
||||
|
||||
type ChannelProps = {
|
||||
channelsCount: number;
|
||||
hasChannels: boolean;
|
||||
isCRTEnabled: boolean;
|
||||
teamsCount: number;
|
||||
hasTeams: boolean;
|
||||
hasMoreThanOneTeam: boolean;
|
||||
isLicensed: boolean;
|
||||
showToS: boolean;
|
||||
launchType: LaunchType;
|
||||
@@ -126,10 +127,10 @@ const ChannelListScreen = (props: ChannelProps) => {
|
||||
}, [theme, insets.top]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.teamsCount) {
|
||||
if (!props.hasTeams) {
|
||||
resetToTeams();
|
||||
}
|
||||
}, [Boolean(props.teamsCount)]);
|
||||
}, [Boolean(props.hasTeams)]);
|
||||
|
||||
useEffect(() => {
|
||||
const back = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
||||
@@ -176,13 +177,13 @@ const ChannelListScreen = (props: ChannelProps) => {
|
||||
>
|
||||
<TeamSidebar
|
||||
iconPad={canAddOtherServers}
|
||||
teamsCount={props.teamsCount}
|
||||
hasMoreThanOneTeam={props.hasMoreThanOneTeam}
|
||||
/>
|
||||
<CategoriesList
|
||||
iconPad={canAddOtherServers && props.teamsCount <= 1}
|
||||
iconPad={canAddOtherServers && !props.hasMoreThanOneTeam}
|
||||
isCRTEnabled={props.isCRTEnabled}
|
||||
teamsCount={props.teamsCount}
|
||||
channelsCount={props.channelsCount}
|
||||
moreThanOneTeam={props.hasMoreThanOneTeam}
|
||||
hasChannels={props.hasChannels}
|
||||
/>
|
||||
{isTablet &&
|
||||
<AdditionalTabletView/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryAllMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {observeCurrentTeamId, observeLicense} from '@queries/servers/system';
|
||||
@@ -21,11 +21,22 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
switchMap((lcs) => (lcs ? of$(lcs.IsLicensed === 'true') : of$(false))),
|
||||
);
|
||||
|
||||
const teamsCount = queryMyTeams(database).observeCount(false);
|
||||
|
||||
return {
|
||||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
teamsCount: queryMyTeams(database).observeCount(false),
|
||||
channelsCount: observeCurrentTeamId(database).pipe(
|
||||
hasTeams: teamsCount.pipe(
|
||||
switchMap((v) => of$(v > 0)),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
hasMoreThanOneTeam: teamsCount.pipe(
|
||||
switchMap((v) => of$(v > 1)),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
hasChannels: observeCurrentTeamId(database).pipe(
|
||||
switchMap((id) => (id ? queryAllMyChannelsForTeam(database, id).observeCount(false) : of$(0))),
|
||||
switchMap((v) => of$(v > 0)),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
isLicensed,
|
||||
showToS: observeShowToS(database),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {useIsFocused, useNavigation} from '@react-navigation/native';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, LayoutChangeEvent, Platform, StyleSheet, ViewStyle} from 'react-native';
|
||||
import HWKeyboardEvent from 'react-native-hw-keyboard-event';
|
||||
import Animated, {useAnimatedStyle, useDerivedValue, withTiming} from 'react-native-reanimated';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
@@ -16,12 +17,14 @@ import FreezeScreen from '@components/freeze_screen';
|
||||
import Loading from '@components/loading';
|
||||
import NavigationHeader from '@components/navigation_header';
|
||||
import RoundedHeaderContext from '@components/rounded_header_context';
|
||||
import {Screens} from '@constants';
|
||||
import {BOTTOM_TAB_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useKeyboardHeight} from '@hooks/device';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {FileFilter, FileFilters, filterFileExtensions} from '@utils/file';
|
||||
import {TabTypes, TabType} from '@utils/search';
|
||||
|
||||
@@ -317,6 +320,19 @@ const SearchScreen = ({teamId, teams}: Props) => {
|
||||
}
|
||||
}, [isFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = HWKeyboardEvent.onHWKeyPressed((keyEvent: {pressedKey: string}) => {
|
||||
const topScreen = NavigationStore.getVisibleScreen();
|
||||
if (topScreen === Screens.HOME && isFocused && keyEvent.pressedKey === 'enter') {
|
||||
searchRef.current?.blur();
|
||||
onSubmit();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
}, [onSubmit]);
|
||||
|
||||
return (
|
||||
<FreezeScreen freeze={!isFocused}>
|
||||
<NavigationHeader
|
||||
|
||||
@@ -31,12 +31,13 @@ import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type ReactionModel from '@typings/database/models/servers/reaction';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type EnhancedProps = WithDatabaseArgs & {
|
||||
combinedPost?: Post | PostModel;
|
||||
post: PostModel;
|
||||
showAddReaction: boolean;
|
||||
location: string;
|
||||
sourceScreen: AvailableScreens;
|
||||
serverUrl: string;
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ const withPost = withObservables([], ({post, database}: {post: Post | PostModel}
|
||||
};
|
||||
});
|
||||
|
||||
const enhanced = withObservables([], ({combinedPost, post, showAddReaction, location, database, serverUrl}: EnhancedProps) => {
|
||||
const enhanced = withObservables([], ({combinedPost, post, showAddReaction, sourceScreen, database, serverUrl}: EnhancedProps) => {
|
||||
const channel = observeChannel(database, post.channelId);
|
||||
const channelIsArchived = channel.pipe(switchMap((ch: ChannelModel) => of$(ch.deleteAt !== 0)));
|
||||
const currentUser = observeCurrentUser(database);
|
||||
@@ -112,7 +113,7 @@ const enhanced = withObservables([], ({combinedPost, post, showAddReaction, loca
|
||||
);
|
||||
|
||||
const canReply = combineLatest([channelIsArchived, channelIsReadOnly, canPostPermission]).pipe(switchMap(([isArchived, isReadOnly, canPost]) => {
|
||||
return of$(!isArchived && !isReadOnly && location !== Screens.THREAD && !isSystemMessage(post) && canPost);
|
||||
return of$(!isArchived && !isReadOnly && sourceScreen !== Screens.THREAD && !isSystemMessage(post) && canPost);
|
||||
}));
|
||||
|
||||
const canPin = combineLatest([channelIsArchived, channelIsReadOnly]).pipe(switchMap(([isArchived, isReadOnly]) => {
|
||||
|
||||
@@ -1,6 +1,200 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General, Preferences} from '@constants';
|
||||
import {DMS_CATEGORY} from '@constants/categories';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export type ChannelWithMyChannel = {
|
||||
channel: ChannelModel;
|
||||
myChannel: MyChannelModel;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export function makeCategoryChannelId(teamId: string, channelId: string) {
|
||||
return `${teamId}_${channelId}`;
|
||||
}
|
||||
|
||||
export const isUnreadChannel = (myChannel: MyChannelModel, notifyProps?: Partial<ChannelNotifyProps>, lastUnreadChannelId?: string) => {
|
||||
const isMuted = notifyProps?.mark_unread === General.MENTION;
|
||||
return (isMuted && myChannel.mentionsCount) || (!isMuted && myChannel.isUnread) || (myChannel.id === lastUnreadChannelId);
|
||||
};
|
||||
|
||||
export const filterArchivedChannels = (channelsWithMyChannel: ChannelWithMyChannel[], currentChannelId: string) => {
|
||||
return channelsWithMyChannel.filter((cwm) => cwm.channel.deleteAt === 0 || cwm.channel.id === currentChannelId);
|
||||
};
|
||||
|
||||
export const filterAutoclosedDMs = (
|
||||
categoryType: CategoryType, limit: number, currentChannelId: string,
|
||||
channelsWithMyChannel: ChannelWithMyChannel[], preferences: PreferenceModel[],
|
||||
notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>,
|
||||
deactivatedDMs?: Map<string, UserModel | undefined >,
|
||||
lastUnreadChannelId?: string,
|
||||
) => {
|
||||
if (categoryType !== DMS_CATEGORY) {
|
||||
// Only autoclose DMs that haven't been assigned to a category
|
||||
return channelsWithMyChannel;
|
||||
}
|
||||
const prefMap = preferences.reduce((acc, v) => {
|
||||
const existing = acc.get(v.name);
|
||||
acc.set(v.name, Math.max((v.value as unknown as number) || 0, existing || 0));
|
||||
return acc;
|
||||
}, new Map<string, number>());
|
||||
const getLastViewedAt = (cwm: ChannelWithMyChannel) => {
|
||||
// The server only ever sets the last_viewed_at to the time of the last post in channel, so we may need
|
||||
// to use the preferences added for the previous version of autoclosing DMs.
|
||||
const id = cwm.channel.id;
|
||||
return Math.max(
|
||||
cwm.myChannel.lastViewedAt,
|
||||
prefMap.get(id) || 0,
|
||||
);
|
||||
};
|
||||
|
||||
let unreadCount = 0;
|
||||
let visibleChannels = channelsWithMyChannel.filter((cwm) => {
|
||||
const {channel, myChannel} = cwm;
|
||||
if (myChannel.isUnread) {
|
||||
unreadCount++;
|
||||
|
||||
// Unread DMs/GMs are always visible
|
||||
return true;
|
||||
}
|
||||
|
||||
if (channel.id === currentChannelId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// DMs with deactivated users will be visible if you're currently viewing them and they were opened
|
||||
// since the user was deactivated
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
const lastViewedAt = getLastViewedAt(cwm);
|
||||
const teammate = deactivatedDMs?.get(channel.id);
|
||||
if (teammate && teammate.deleteAt > lastViewedAt) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
visibleChannels.sort((cwmA, cwmB) => {
|
||||
const channelA = cwmA.channel;
|
||||
const channelB = cwmB.channel;
|
||||
const myChannelA = cwmA.myChannel;
|
||||
const myChannelB = cwmB.myChannel;
|
||||
|
||||
// Should always prioritise the current channel
|
||||
if (channelA.id === currentChannelId) {
|
||||
return -1;
|
||||
} else if (channelB.id === currentChannelId) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Second priority is for unread channels
|
||||
const isUnreadA = isUnreadChannel(myChannelA, notifyPropsPerChannel[myChannelA.id], lastUnreadChannelId);
|
||||
const isUnreadB = isUnreadChannel(myChannelB, notifyPropsPerChannel[myChannelB.id], lastUnreadChannelId);
|
||||
if (isUnreadA && !isUnreadB) {
|
||||
return -1;
|
||||
} else if (isUnreadB && !isUnreadA) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Third priority is last_viewed_at
|
||||
const channelAlastViewed = getLastViewedAt(cwmA) || 0;
|
||||
const channelBlastViewed = getLastViewedAt(cwmB) || 0;
|
||||
|
||||
if (channelAlastViewed > channelBlastViewed) {
|
||||
return -1;
|
||||
} else if (channelBlastViewed > channelAlastViewed) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
// The limit of DMs user specifies to be rendered in the sidebar
|
||||
const remaining = Math.max(limit, unreadCount);
|
||||
visibleChannels = visibleChannels.slice(0, remaining);
|
||||
|
||||
return visibleChannels;
|
||||
};
|
||||
|
||||
export const filterManuallyClosedDms = (
|
||||
channelsWithMyChannel: ChannelWithMyChannel[],
|
||||
notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>,
|
||||
preferences: PreferenceModel[],
|
||||
currentUserId: string,
|
||||
lastUnreadChannelId?: string,
|
||||
) => {
|
||||
return channelsWithMyChannel.filter((cwm) => {
|
||||
const {channel, myChannel} = cwm;
|
||||
|
||||
if (!isDMorGM(channel)) {
|
||||
return true;
|
||||
} else if (!myChannel.lastPostAt) {
|
||||
// If the direct channel does not have posts we hide it
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isUnreadChannel(myChannel, notifyPropsPerChannel[myChannel.id], lastUnreadChannelId)) {
|
||||
// Unread DMs/GMs are always visible
|
||||
return true;
|
||||
}
|
||||
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
const teammateId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
return getPreferenceAsBool(preferences, Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, teammateId, true);
|
||||
}
|
||||
|
||||
return getPreferenceAsBool(preferences, Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, channel.id, true);
|
||||
});
|
||||
};
|
||||
|
||||
const sortChannelsByName = (notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>, locale: string) => {
|
||||
return (a: ChannelWithMyChannel, b: ChannelWithMyChannel) => {
|
||||
// Sort muted channels last
|
||||
const aMuted = notifyPropsPerChannel[a.channel.id]?.mark_unread === General.MENTION;
|
||||
const bMuted = notifyPropsPerChannel[b.channel.id]?.mark_unread === General.MENTION;
|
||||
|
||||
if (aMuted && !bMuted) {
|
||||
return 1;
|
||||
} else if (!aMuted && bMuted) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// And then sort alphabetically
|
||||
return a.channel.displayName.localeCompare(b.channel.displayName, locale, {numeric: true});
|
||||
};
|
||||
};
|
||||
|
||||
export const sortChannels = (sorting: CategorySorting, channelsWithMyChannel: ChannelWithMyChannel[], notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>, locale: string) => {
|
||||
if (sorting === 'recent') {
|
||||
return channelsWithMyChannel.sort((cwmA, cwmB) => {
|
||||
return cwmB.myChannel.lastPostAt - cwmA.myChannel.lastPostAt;
|
||||
}).map((cwm) => cwm.channel);
|
||||
} else if (sorting === 'manual') {
|
||||
return channelsWithMyChannel.sort((cwmA, cwmB) => {
|
||||
return cwmA.sortOrder - cwmB.sortOrder;
|
||||
}).map((cwm) => cwm.channel);
|
||||
}
|
||||
|
||||
const sortByName = sortChannelsByName(notifyPropsPerChannel, locale);
|
||||
return channelsWithMyChannel.sort(sortByName).map((cwm) => cwm.channel);
|
||||
};
|
||||
|
||||
export const getUnreadIds = (cwms: ChannelWithMyChannel[], notifyPropsPerChannel: Record<string, Partial<ChannelNotifyProps>>, lastUnreadId?: string) => {
|
||||
return cwms.reduce<Set<string>>((result, cwm) => {
|
||||
if (isUnreadChannel(cwm.myChannel, notifyPropsPerChannel, lastUnreadId)) {
|
||||
result.add(cwm.channel.id);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, new Set());
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
import {isMinimumServerVersion} from './helpers';
|
||||
|
||||
export function hasReliableWebsocket(config: ClientConfig) {
|
||||
if (isMinimumServerVersion(config.Version, 6, 5)) {
|
||||
export function hasReliableWebsocket(version?: string, reliableWebsocketsConfig?: string) {
|
||||
if (version && isMinimumServerVersion(version, 6, 5)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return config.EnableReliableWebSockets === 'true';
|
||||
return reliableWebsocketsConfig === 'true';
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ export function doesMatchNamedEmoji(emojiName: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export const getEmojiFirstAlias = (emoji: string) => {
|
||||
export const getEmojiFirstAlias = (emoji: string): string => {
|
||||
return getEmojiByName(emoji, [])?.short_names?.[0] || emoji;
|
||||
};
|
||||
|
||||
|
||||
@@ -83,8 +83,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false
|
||||
let rootId = notification.userInfo["root_id"] as? String ?? ""
|
||||
let channelName = notification.userInfo["channel_name"] as? String ?? ""
|
||||
let message = (notification.userInfo["message"] as? String ?? "")
|
||||
let senderName = notification.userInfo["sender_name"] as? String
|
||||
let channelName = notification.userInfo["channel_name"] as? String
|
||||
var message = (notification.userInfo["message"] as? String ?? "")
|
||||
let overrideUsername = notification.userInfo["override_username"] as? String
|
||||
let senderId = notification.userInfo["sender_id"] as? String
|
||||
let senderIdentifier = overrideUsername ?? senderId
|
||||
@@ -94,11 +95,18 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
if isCRTEnabled && !rootId.isEmpty {
|
||||
conversationId = rootId
|
||||
}
|
||||
|
||||
if channelName == nil && message == "",
|
||||
let senderName = senderName,
|
||||
let body = bestAttemptContent?.body {
|
||||
message = body.replacingOccurrences(of: "\(senderName) ", with: "")
|
||||
bestAttemptContent?.body = message
|
||||
}
|
||||
|
||||
let handle = INPersonHandle(value: senderIdentifier, type: .unknown)
|
||||
let sender = INPerson(personHandle: handle,
|
||||
nameComponents: nil,
|
||||
displayName: channelName,
|
||||
displayName: channelName ?? senderName,
|
||||
image: avatar,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil)
|
||||
|
||||
@@ -54,7 +54,7 @@ target 'Mattermost' do
|
||||
pod 'simdjson', path: '../node_modules/@nozbe/simdjson'
|
||||
|
||||
# TODO: Remove this once upstream PR https://github.com/daltoniam/Starscream/pull/871 is merged
|
||||
pod 'Starscream', :git => 'https://github.com/mattermost/Starscream.git', :commit => '2770c931b2758f26e29b937d547a23122e9c6583'
|
||||
pod 'Starscream', :git => 'https://github.com/mattermost/Starscream.git', :commit => '9575b6781d1262247096af73617ae3acb2b139a0'
|
||||
|
||||
end
|
||||
|
||||
|
||||
@@ -709,7 +709,7 @@ DEPENDENCIES:
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||
- "simdjson (from `../node_modules/@nozbe/simdjson`)"
|
||||
- Starscream (from `https://github.com/mattermost/Starscream.git`, commit `2770c931b2758f26e29b937d547a23122e9c6583`)
|
||||
- Starscream (from `https://github.com/mattermost/Starscream.git`, commit `9575b6781d1262247096af73617ae3acb2b139a0`)
|
||||
- "WatermelonDB (from `../node_modules/@nozbe/watermelondb`)"
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
@@ -908,7 +908,7 @@ EXTERNAL SOURCES:
|
||||
simdjson:
|
||||
:path: "../node_modules/@nozbe/simdjson"
|
||||
Starscream:
|
||||
:commit: 2770c931b2758f26e29b937d547a23122e9c6583
|
||||
:commit: 9575b6781d1262247096af73617ae3acb2b139a0
|
||||
:git: https://github.com/mattermost/Starscream.git
|
||||
WatermelonDB:
|
||||
:path: "../node_modules/@nozbe/watermelondb"
|
||||
@@ -917,7 +917,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
Starscream:
|
||||
:commit: 2770c931b2758f26e29b937d547a23122e9c6583
|
||||
:commit: 9575b6781d1262247096af73617ae3acb2b139a0
|
||||
:git: https://github.com/mattermost/Starscream.git
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
@@ -1033,6 +1033,6 @@ SPEC CHECKSUMS:
|
||||
Yoga: 5ed1699acbba8863755998a4245daa200ff3817b
|
||||
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
|
||||
|
||||
PODFILE CHECKSUM: 9f76739ed16bbdc0f4b1049ecb366fb5d23a0f3a
|
||||
PODFILE CHECKSUM: 831b649321a4d14a86a074af619aa779ebc048c4
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -19,7 +19,7 @@
|
||||
"@gorhom/bottom-sheet": "4.4.5",
|
||||
"@mattermost/compass-icons": "0.1.35",
|
||||
"@mattermost/react-native-emm": "1.3.5",
|
||||
"@mattermost/react-native-network-client": "1.3.1",
|
||||
"@mattermost/react-native-network-client": "1.3.2",
|
||||
"@mattermost/react-native-paste-input": "0.6.2",
|
||||
"@mattermost/react-native-turbo-log": "0.2.3",
|
||||
"@mattermost/react-native-turbo-mailer": "0.2.4",
|
||||
@@ -3241,9 +3241,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mattermost/react-native-network-client": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.1.tgz",
|
||||
"integrity": "sha512-DtwVLV/NUE6MkXOlVZG+4QJXou6nHMdmsxnP1+RqhOeSw5jJlQvxmQgxzxvxLpaWOag+wgB1zpDulGNbr/Cz6Q==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.2.tgz",
|
||||
"integrity": "sha512-3GFNzMXZWlIXXDYQLIJlKRf+HUZKP0F7mpZ1rSTgoTmUeFdqde4uRiU/L96COg34rAdeFRFrgpk0DxEnT7NiVg==",
|
||||
"dependencies": {
|
||||
"validator": "13.9.0",
|
||||
"zod": "3.20.6"
|
||||
@@ -24297,9 +24297,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@mattermost/react-native-network-client": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.1.tgz",
|
||||
"integrity": "sha512-DtwVLV/NUE6MkXOlVZG+4QJXou6nHMdmsxnP1+RqhOeSw5jJlQvxmQgxzxvxLpaWOag+wgB1zpDulGNbr/Cz6Q==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.2.tgz",
|
||||
"integrity": "sha512-3GFNzMXZWlIXXDYQLIJlKRf+HUZKP0F7mpZ1rSTgoTmUeFdqde4uRiU/L96COg34rAdeFRFrgpk0DxEnT7NiVg==",
|
||||
"requires": {
|
||||
"validator": "13.9.0",
|
||||
"zod": "3.20.6"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@gorhom/bottom-sheet": "4.4.5",
|
||||
"@mattermost/compass-icons": "0.1.35",
|
||||
"@mattermost/react-native-emm": "1.3.5",
|
||||
"@mattermost/react-native-network-client": "1.3.1",
|
||||
"@mattermost/react-native-network-client": "1.3.2",
|
||||
"@mattermost/react-native-paste-input": "0.6.2",
|
||||
"@mattermost/react-native-turbo-log": "0.2.3",
|
||||
"@mattermost/react-native-turbo-mailer": "0.2.4",
|
||||
|
||||
@@ -25,7 +25,7 @@ declare class CategoryModel extends Model {
|
||||
displayName: string;
|
||||
|
||||
/** type : The type of category */
|
||||
type: string;
|
||||
type: CategoryType;
|
||||
|
||||
/** sort_order : The sort order for this category */
|
||||
sortOrder: number;
|
||||
|
||||
Reference in New Issue
Block a user