Compare commits

...

15 commits
main ... v2.2.0

Author SHA1 Message Date
Daniel Espino García
f6c20b764b
Bump app version number to 2.2.0 and build number to 462 (#7217) 2023-03-17 10:30:04 +01:00
Mattermost Build
86e2d4aacd
Fix muted channels not being filtered out on the categories list (#7210) (#7212)
(cherry picked from commit ba523fab15)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-03-17 10:12:27 +01:00
Daniel Espino García
8cbee57e3e
Add missing podfile change (#7203) 2023-03-14 10:39:29 +01:00
Daniel Espino García
15fd6925b3 Performance fixes and fix manual sort (#7190)
* Performance fixes and fix manual sort

* Fix test

* Use combineLatestWith

* Revert unread on top
2023-03-07 19:25:25 +01:00
Daniel Espino García
571070e284 Fix race condition when the same websocket gets initialized twice (#7185)
* Fix race condition when the same websocket gets initialized twice

* Bump network library
2023-03-07 19:13:19 +01:00
Elias Nahum
ab8a43032e
Refactor category channels to react to setting changes and apply the correct order (#7170)
* Refactor category channels to react to setting changes and apply the correct order

* feedback review
2023-03-03 15:54:12 +02:00
Elias Nahum
6904be23da
Fix push notification token registration race/missing (#7183) 2023-03-03 12:14:32 +02:00
Elias Nahum
6bc7c05ccb
support WS connection over TLS1.3 (#7182)
* support WS connection over TLS1.3

* fix updateDraftMessage on unmount
2023-03-03 11:33:48 +02:00
Elias Nahum
4b142483a5
Fix display name when open own DM (#7181) 2023-03-02 16:58:31 +02:00
Elias Nahum
63674e2a43
fix entry for tablets (#7179) 2023-03-02 16:56:26 +02:00
Elias Nahum
cdaf1f50e7
use sourceScreen instead of location in post options (#7176) 2023-03-02 12:47:58 +02:00
Elias Nahum
10735dcbf1
trigger Search when hardware keyboard enter key is pressed (#7174) 2023-03-01 15:20:02 +02:00
Elias Nahum
619decd253
Fix potential reaction crash (#7172) 2023-03-01 15:19:55 +02:00
Elias Nahum
55f18bcfc3
ignore leading and trailing spaces when editing profile (#7173) 2023-03-01 15:19:47 +02:00
Elias Nahum
870336142a
Fix iOS push notification when set as generic message with sender name (#7171) 2023-03-01 15:19:39 +02:00
41 changed files with 579 additions and 369 deletions

View file

@ -110,8 +110,8 @@ android {
applicationId "com.mattermost.rnbeta" applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 459 versionCode 462
versionName "2.1.0" versionName "2.2.0"
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }

View file

@ -11,6 +11,7 @@ import com.mattermost.helpers.database_extension.queryCurrentUserId
import com.nozbe.watermelondb.Database import com.nozbe.watermelondb.Database
import java.text.Collator import java.text.Collator
import java.util.Locale 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?> { 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") val channel = fetch(serverUrl, "/api/v4/channels/$channelId")

View file

@ -1,8 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {markChannelAsViewed} from '@actions/local/channel'; import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel';
import {fetchGroupsForMember} from '@actions/remote/groups'; import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post'; import {fetchPostsForUnreadChannels} from '@actions/remote/post';
import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference'; import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
@ -440,9 +439,9 @@ export async function handleEntryAfterLoadNavigation(
if (!currentTeamIdAfterLoad) { if (!currentTeamIdAfterLoad) {
// First load or no team // First load or no team
if (tabletDevice) { if (tabletDevice) {
await setCurrentTeamAndChannelId(operator, initialTeamId, '');
} else {
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId); await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
} else {
await setCurrentTeamAndChannelId(operator, initialTeamId, '');
} }
} else if (currentTeamIdAfterLoad !== currentTeamId) { } else if (currentTeamIdAfterLoad !== currentTeamId) {
// Switched teams while loading // Switched teams while loading
@ -466,9 +465,6 @@ export async function handleEntryAfterLoadNavigation(
} else { } else {
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId); await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
} }
} else if (tabletDevice && initialChannelId === currentChannelId) {
await markChannelAsRead(serverUrl, initialChannelId);
markChannelAsViewed(serverUrl, initialChannelId);
} }
} catch (error) { } catch (error) {
logDebug('could not manage the entry after load navigation', error); logDebug('could not manage the entry after load navigation', error);

View file

@ -130,6 +130,13 @@ async function doReconnect(serverUrl: string) {
if (models?.length) { if (models?.length) {
await operator.batchRecords(models, 'doReconnect'); 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`); logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
setTeamLoading(serverUrl, false); setTeamLoading(serverUrl, false);

View file

@ -6,7 +6,7 @@ import {Platform} from 'react-native';
import {WebsocketEvents} from '@constants'; import {WebsocketEvents} from '@constants';
import DatabaseManager from '@database/manager'; import DatabaseManager from '@database/manager';
import {getConfig} from '@queries/servers/system'; import {getConfigValue} from '@queries/servers/system';
import {hasReliableWebsocket} from '@utils/config'; import {hasReliableWebsocket} from '@utils/config';
import {toMilliseconds} from '@utils/datetime'; import {toMilliseconds} from '@utils/datetime';
import {logError, logInfo, logWarning} from '@utils/log'; import {logError, logInfo, logWarning} from '@utils/log';
@ -79,8 +79,12 @@ export default class WebSocketClient {
return; return;
} }
const config = await getConfig(database); const [websocketUrl, version, reliableWebsocketConfig] = await Promise.all([
const connectionUrl = (config.WebsocketURL || this.serverUrl) + '/api/v4/websocket'; getConfigValue(database, 'WebsocketURL'),
getConfigValue(database, 'Version'),
getConfigValue(database, 'EnableReliableWebSockets'),
]);
const connectionUrl = (websocketUrl || this.serverUrl) + '/api/v4/websocket';
if (this.connectingCallback) { if (this.connectingCallback) {
this.connectingCallback(); this.connectingCallback();
@ -101,7 +105,7 @@ export default class WebSocketClient {
this.url = connectionUrl; this.url = connectionUrl;
const reliableWebSockets = hasReliableWebsocket(config); const reliableWebSockets = hasReliableWebsocket(version, reliableWebsocketConfig);
if (reliableWebSockets) { if (reliableWebSockets) {
// Add connection id, and last_sequence_number to the query param. // 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. // 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}`; headers.Authorization = `Bearer ${this.token}`;
} }
const {client} = await getOrCreateWebSocketClient(this.url, {headers, timeoutInterval: WEBSOCKET_TIMEOUT}); 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; this.conn = client;
} catch (error) { } catch (error) {
return; return;

View file

@ -283,9 +283,9 @@ export default function PostInput({
}); });
return () => { return () => {
listener.remove(); 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(() => { useEffect(() => {
if (value !== lastNativeValue.current) { if (value !== lastNativeValue.current) {

View file

@ -108,7 +108,7 @@ const CombinedUserActivity = ({
return; return;
} }
const passProps = {post}; const passProps = {post, sourceScreen: location};
Keyboard.dismiss(); Keyboard.dismiss();
const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : ''; const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : '';
@ -117,7 +117,7 @@ const CombinedUserActivity = ({
} else { } else {
showModalOverCurrentContext(Screens.POST_OPTIONS, passProps, bottomSheetModalOptions(theme)); showModalOverCurrentContext(Screens.POST_OPTIONS, passProps, bottomSheetModalOptions(theme));
} }
}, [post, canDelete, isTablet, intl]); }, [post, canDelete, isTablet, intl, location]);
const renderMessage = (postType: string, userIds: string[], actorId: string) => { const renderMessage = (postType: string, userIds: string[], actorId: string) => {
let actor = ''; let actor = '';

View file

@ -86,11 +86,11 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
if (reaction) { if (reaction) {
const emojiAlias = getEmojiFirstAlias(reaction.emojiName); const emojiAlias = getEmojiFirstAlias(reaction.emojiName);
if (acc.has(emojiAlias)) { if (acc.has(emojiAlias)) {
const rs = acc.get(emojiAlias); const rs = acc.get(emojiAlias)!;
// eslint-disable-next-line max-nested-callbacks // 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) { if (!present) {
rs!.push(reaction); rs.push(reaction);
} }
} else { } else {
acc.set(emojiAlias, [reaction]); acc.set(emojiAlias, [reaction]);
@ -105,7 +105,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
}, new Map<string, ReactionModel[]>()); }, new Map<string, ReactionModel[]>());
return {reactionsByName, highlightedReactions}; return {reactionsByName, highlightedReactions};
}, [sortedReactions]); }, [sortedReactions, reactions]);
const handleAddReactionToPost = (emoji: string) => { const handleAddReactionToPost = (emoji: string) => {
addReaction(serverUrl, postId, emoji); addReaction(serverUrl, postId, emoji);
@ -178,7 +178,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
return ( return (
<Reaction <Reaction
key={r} key={r}
count={reaction!.length} count={reaction?.length || 1}
emojiName={r} emojiName={r}
highlight={highlightedReactions.includes(r)} highlight={highlightedReactions.includes(r)}
onPress={handleReactionPress} onPress={handleReactionPress}

View file

@ -150,6 +150,7 @@ export default function SelectedUsers({
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const numberSelectedIds = Object.keys(selectedIds).length; const numberSelectedIds = Object.keys(selectedIds).length;
const bottomSpace = (dimensions.height - containerHeight - modalPosition); const bottomSpace = (dimensions.height - containerHeight - modalPosition);
const bottomPaddingBottom = isTablet ? CHIP_HEIGHT_WITH_MARGIN : 0;
const users = useMemo(() => { const users = useMemo(() => {
const u = []; const u = [];
@ -172,8 +173,8 @@ export default function SelectedUsers({
}, [selectedIds, teammateNameDisplay, onRemove]); }, [selectedIds, teammateNameDisplay, onRemove]);
const totalPanelHeight = useDerivedValue(() => ( const totalPanelHeight = useDerivedValue(() => (
isVisible ? panelHeight.value + BUTTON_HEIGHT : 0 isVisible ? panelHeight.value + BUTTON_HEIGHT + bottomPaddingBottom : 0
), [isVisible, isTablet]); ), [isVisible, isTablet, bottomPaddingBottom]);
const marginBottom = useMemo(() => { const marginBottom = useMemo(() => {
let margin = keyboard.height && Platform.OS === 'ios' ? keyboard.height - insets.bottom : 0; let margin = keyboard.height && Platform.OS === 'ios' ? keyboard.height - insets.bottom : 0;
@ -208,7 +209,7 @@ export default function SelectedUsers({
}, [onPress]); }, [onPress]);
const onLayout = useCallback((e: LayoutChangeEvent) => { 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({ const androidMaxHeight = Platform.select({
@ -235,8 +236,8 @@ export default function SelectedUsers({
const animatedViewStyle = useAnimatedStyle(() => ({ const animatedViewStyle = useAnimatedStyle(() => ({
height: withTiming(totalPanelHeight.value + insets.bottom, {duration: 250}), height: withTiming(totalPanelHeight.value + insets.bottom, {duration: 250}),
borderWidth: isVisible ? 1 : 0, borderWidth: isVisible ? 1 : 0,
maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT + insets.bottom : 0, maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT + bottomPaddingBottom + insets.bottom : 0,
}), [isVisible, insets]); }), [isVisible, insets, bottomPaddingBottom]);
const animatedButtonStyle = useAnimatedStyle(() => ({ const animatedButtonStyle = useAnimatedStyle(() => ({
opacity: withTiming(isVisible ? 1 : 0, {duration: isVisible ? 500 : 100}), opacity: withTiming(isVisible ? 1 : 0, {duration: isVisible ? 500 : 100}),

View file

@ -14,7 +14,7 @@ import TeamList from './team_list';
type Props = { type Props = {
iconPad?: boolean; iconPad?: boolean;
canJoinOtherTeams: boolean; canJoinOtherTeams: boolean;
teamsCount: number; hasMoreThanOneTeam: boolean;
} }
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -36,8 +36,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
}; };
}); });
export default function TeamSidebar({iconPad, canJoinOtherTeams, teamsCount}: Props) { export default function TeamSidebar({iconPad, canJoinOtherTeams, hasMoreThanOneTeam}: Props) {
const initialWidth = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0; const initialWidth = hasMoreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0;
const width = useSharedValue(initialWidth); const width = useSharedValue(initialWidth);
const marginTop = useSharedValue(iconPad ? 44 : 0); const marginTop = useSharedValue(iconPad ? 44 : 0);
const theme = useTheme(); const theme = useTheme();
@ -58,8 +58,8 @@ export default function TeamSidebar({iconPad, canJoinOtherTeams, teamsCount}: Pr
}, [iconPad]); }, [iconPad]);
useEffect(() => { useEffect(() => {
width.value = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0; width.value = hasMoreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0;
}, [teamsCount]); }, [hasMoreThanOneTeam]);
return ( return (
<Animated.View style={[styles.container, transform]}> <Animated.View style={[styles.container, transform]}>

View file

@ -3,6 +3,8 @@
export const CATEGORIES_TO_KEEP: Record<string, string> = { export const CATEGORIES_TO_KEEP: Record<string, string> = {
ADVANCED_SETTINGS: 'advanced_settings', ADVANCED_SETTINGS: 'advanced_settings',
CHANNEL_APPROXIMATE_VIEW_TIME: 'channel_approximate_view_time',
CHANNEL_OPEN_TIME: 'channel_open_time',
DIRECT_CHANNEL_SHOW: 'direct_channel_show', DIRECT_CHANNEL_SHOW: 'direct_channel_show',
GROUP_CHANNEL_SHOW: 'group_channel_show', GROUP_CHANNEL_SHOW: 'group_channel_show',
DISPLAY_SETTINGS: 'display_settings', DISPLAY_SETTINGS: 'display_settings',

View file

@ -246,10 +246,11 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
const totalMsg = isCRT ? channel.total_msg_count_root! : channel.total_msg_count; const totalMsg = isCRT ? channel.total_msg_count_root! : channel.total_msg_count;
const myMsgCount = isCRT ? my.msg_count_root! : my.msg_count; const myMsgCount = isCRT ? my.msg_count_root! : my.msg_count;
const msgCount = Math.max(0, totalMsg - myMsgCount); 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.msg_count = msgCount;
my.mention_count = isCRT ? my.mention_count_root! : my.mention_count; my.mention_count = isCRT ? my.mention_count_root! : my.mention_count;
my.is_unread = msgCount > 0; 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 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) || if ((chan && e.lastPostAt < lastPostAt) ||
e.isUnread !== my.is_unread || e.lastViewedAt < my.last_viewed_at || e.isUnread !== my.is_unread || e.lastViewedAt < my.last_viewed_at ||
e.roles !== my.roles e.roles !== my.roles

View file

@ -37,22 +37,22 @@ class PushNotifications {
configured = false; configured = false;
init(register: boolean) { init(register: boolean) {
if (register) {
this.registerIfNeeded();
}
Notifications.events().registerNotificationOpened(this.onNotificationOpened); Notifications.events().registerNotificationOpened(this.onNotificationOpened);
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered); Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground); Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground); Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
if (register) {
this.registerIfNeeded();
}
} }
async registerIfNeeded() { async registerIfNeeded() {
const isRegistered = await Notifications.isRegisteredForRemoteNotifications(); const isRegistered = await Notifications.isRegisteredForRemoteNotifications();
if (!isRegistered) { if (!isRegistered) {
await requestNotifications(['alert', 'sound', 'badge']); await requestNotifications(['alert', 'sound', 'badge']);
Notifications.registerRemoteNotifications();
} }
Notifications.registerRemoteNotifications();
} }
createReplyCategory = () => { createReplyCategory = () => {

View file

@ -11,14 +11,11 @@ import {makeCategoryChannelId} from '@utils/categories';
import {pluckUnique} from '@utils/helpers'; import {pluckUnique} from '@utils/helpers';
import {logDebug} from '@utils/log'; import {logDebug} from '@utils/log';
import {observeChannelsByLastPostAt} from './channel';
import type ServerDataOperator from '@database/operator/server_data_operator'; import type ServerDataOperator from '@database/operator/server_data_operator';
import type CategoryModel from '@typings/database/models/servers/category'; import type CategoryModel from '@typings/database/models/servers/category';
import type CategoryChannelModel from '@typings/database/models/servers/category_channel'; 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) => { export const getCategoryById = async (database: Database, categoryId: string) => {
try { try {
@ -144,24 +141,3 @@ export const observeIsChannelFavorited = (database: Database, teamId: string, ch
distinctUntilChanged(), 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)),
);
};

View file

@ -11,6 +11,7 @@ import {General, Permissions} from '@constants';
import {MM_TABLES} from '@constants/database'; import {MM_TABLES} from '@constants/database';
import {sanitizeLikeString} from '@helpers/database'; import {sanitizeLikeString} from '@helpers/database';
import {hasPermission} from '@utils/role'; import {hasPermission} from '@utils/role';
import {getUserIdFromChannelName} from '@utils/user';
import {prepareDeletePost} from './post'; import {prepareDeletePost} from './post';
import {queryRoles} from './role'; 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) => { export const queryMyChannelUnreads = (database: Database, currentTeamId: string) => {
return database.get<MyChannelModel>(MY_CHANNEL).query( return database.get<MyChannelModel>(MY_CHANNEL).query(
Q.on( Q.on(
@ -453,40 +450,42 @@ export const queryMyChannelUnreads = (database: Database, currentTeamId: string)
Q.where('delete_at', Q.eq(0)), 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), 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) => { 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)), Q.where('delete_at', Q.gt(0)),
).observe().pipe( ).observe();
switchMap((users) => of$(users.map((u) => u.id))),
);
return deactivatedIds.pipe( return deactivated.pipe(
switchMap((dIds) => { switchMap((users) => {
const usersMap = new Map(users.map((u) => [u.id, u]));
return database.get<ChannelModel>(CHANNEL).query( return database.get<ChannelModel>(CHANNEL).query(
Q.on( Q.on(
CHANNEL_MEMBERSHIP, CHANNEL_MEMBERSHIP,
Q.and( Q.and(
Q.where('user_id', Q.notEq(currentUserId)), 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'), 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))); 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 ids = myChannels.map((c) => c.id);
const idsStr = `'${ids.join("','")}'`; const idsStr = `'${ids.join("','")}'`;
const exclude = excludeIds?.length ? `AND c.id NOT IN ('${excludeIds.join("','")}')` : '';
return database.get<ChannelModel>(CHANNEL).query( return database.get<ChannelModel>(CHANNEL).query(
Q.unsafeSqlQuery(`SELECT DISTINCT c.* FROM ${CHANNEL} c INNER JOIN 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`), ORDER BY CASE mc.last_post_at WHEN 0 THEN c.create_at ELSE mc.last_post_at END DESC`),
).observe(); ).observe();
}; };

View file

@ -197,8 +197,8 @@ export default function CreateDirectMessage({
setSelectedIds((current) => removeProfileFromList(current, id)); setSelectedIds((current) => removeProfileFromList(current, id));
}, []); }, []);
const createDirectChannel = useCallback(async (id: string): Promise<boolean> => { const createDirectChannel = useCallback(async (id: string, selectedUser?: UserProfile): Promise<boolean> => {
const user = selectedIds[id]; const user = selectedUser || selectedIds[id];
const displayName = displayUsername(user, intl.locale, teammateNameDisplay); const displayName = displayUsername(user, intl.locale, teammateNameDisplay);
const result = await makeDirectChannel(serverUrl, id, displayName); const result = await makeDirectChannel(serverUrl, id, displayName);
@ -219,7 +219,7 @@ export default function CreateDirectMessage({
return !result.error; return !result.error;
}, [serverUrl]); }, [serverUrl]);
const startConversation = useCallback(async (selectedId?: {[id: string]: boolean}) => { const startConversation = useCallback(async (selectedId?: {[id: string]: boolean}, selectedUser?: UserProfile) => {
if (startingConversation) { if (startingConversation) {
return; return;
} }
@ -233,7 +233,7 @@ export default function CreateDirectMessage({
} else if (idsToUse.length > 1) { } else if (idsToUse.length > 1) {
success = await createGroupChannel(idsToUse); success = await createGroupChannel(idsToUse);
} else { } else {
success = await createDirectChannel(idsToUse[0]); success = await createDirectChannel(idsToUse[0], selectedUser);
} }
if (success) { if (success) {
@ -249,7 +249,7 @@ export default function CreateDirectMessage({
[currentUserId]: true, [currentUserId]: true,
}; };
startConversation(selectedId); startConversation(selectedId, user);
} else { } else {
clearSearch(); clearSearch();
setSelectedIds((current) => { setSelectedIds((current) => {

View file

@ -102,6 +102,7 @@ const EditProfile = ({
popTopScreen(componentId); popTopScreen(componentId);
} }
}, []); }, []);
const enableSaveButton = useCallback((value: boolean) => { const enableSaveButton = useCallback((value: boolean) => {
if (!isTablet) { if (!isTablet) {
const buttons = { const buttons = {
@ -114,18 +115,19 @@ const EditProfile = ({
} }
setCanSave(value); setCanSave(value);
}, [componentId, rightButton]); }, [componentId, rightButton]);
const submitUser = useCallback(preventDoubleTap(async () => { const submitUser = useCallback(preventDoubleTap(async () => {
enableSaveButton(false); enableSaveButton(false);
setError(undefined); setError(undefined);
setUpdating(true); setUpdating(true);
try { try {
const newUserInfo: Partial<UserProfile> = { const newUserInfo: Partial<UserProfile> = {
email: userInfo.email, email: userInfo.email.trim(),
first_name: userInfo.firstName, first_name: userInfo.firstName.trim(),
last_name: userInfo.lastName, last_name: userInfo.lastName.trim(),
nickname: userInfo.nickname, nickname: userInfo.nickname.trim(),
position: userInfo.position, position: userInfo.position.trim(),
username: userInfo.username, username: userInfo.username.trim(),
}; };
const localPath = changedProfilePicture.current?.localPath; const localPath = changedProfilePicture.current?.localPath;
const profileImageRemoved = changedProfilePicture.current?.isRemoved; const profileImageRemoved = changedProfilePicture.current?.isRemoved;

View file

@ -12,29 +12,9 @@ import TestHelper from '@test/test_helper';
import CategoryBody from '.'; import CategoryBody from '.';
import type CategoryModel from '@typings/database/models/servers/category'; 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; 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', () => { describe('components/channel_list/categories/body', () => {
let database: Database; let database: Database;
let category: CategoryModel; let category: CategoryModel;

View file

@ -7,7 +7,6 @@ import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 're
import {fetchDirectChannelsInfo} from '@actions/remote/channel'; import {fetchDirectChannelsInfo} from '@actions/remote/channel';
import ChannelItem from '@components/channel_item'; import ChannelItem from '@components/channel_item';
import {DMS_CATEGORY} from '@constants/categories';
import {useServerUrl} from '@context/server'; import {useServerUrl} from '@context/server';
import {isDMorGM} from '@utils/channel'; import {isDMorGM} from '@utils/channel';
@ -17,7 +16,6 @@ import type ChannelModel from '@typings/database/models/servers/channel';
type Props = { type Props = {
sortedChannels: ChannelModel[]; sortedChannels: ChannelModel[];
category: CategoryModel; category: CategoryModel;
limit: number;
onChannelSwitch: (channelId: string) => void; onChannelSwitch: (channelId: string) => void;
unreadIds: Set<string>; unreadIds: Set<string>;
unreadsOnTop: boolean; unreadsOnTop: boolean;
@ -25,16 +23,13 @@ type Props = {
const extractKey = (item: ChannelModel) => item.id; 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 serverUrl = useServerUrl();
const ids = useMemo(() => { const ids = useMemo(() => {
const filteredChannels = unreadsOnTop ? sortedChannels.filter((c) => !unreadIds.has(c.id)) : sortedChannels; 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; return filteredChannels;
}, [category.type, limit, sortedChannels, unreadIds, unreadsOnTop]); }, [category.type, sortedChannels, unreadIds, unreadsOnTop]);
const unreadChannels = useMemo(() => { const unreadChannels = useMemo(() => {
return unreadsOnTop ? [] : ids.filter((c) => unreadIds.has(c.id)); return unreadsOnTop ? [] : ids.filter((c) => unreadIds.has(c.id));

View file

@ -1,20 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables'; import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs'; import {of as of$, Observable} from 'rxjs';
import {map, switchMap, combineLatestWith} from 'rxjs/operators'; import {switchMap, combineLatestWith, distinctUntilChanged} from 'rxjs/operators';
import {General, Preferences} from '@constants'; import {Preferences} from '@constants';
import {DMS_CATEGORY} from '@constants/categories'; import {DMS_CATEGORY} from '@constants/categories';
import {getSidebarPreferenceAsBool} from '@helpers/api/preference'; import {getSidebarPreferenceAsBool} from '@helpers/api/preference';
import {observeChannelsByCategoryChannelSortOrder, observeChannelsByLastPostAtInCategory} from '@queries/servers/categories'; import {observeArchivedDirectChannels, observeNotifyPropsByChannels} from '@queries/servers/channel';
import {observeArchivedDirectChannels, observeNotifyPropsByChannels, queryChannelsByNames, queryEmptyDirectAndGroupChannels} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName, querySidebarPreferences} from '@queries/servers/preference'; import {queryPreferencesByCategoryAndName, querySidebarPreferences} from '@queries/servers/preference';
import {observeCurrentChannelId, observeCurrentUserId, observeLastUnreadChannelId} from '@queries/servers/system'; 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'; 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 MyChannelModel from '@typings/database/models/servers/my_channel';
import type PreferenceModel from '@typings/database/models/servers/preference'; import type PreferenceModel from '@typings/database/models/servers/preference';
type ChannelData = Pick<ChannelModel, 'id' | 'displayName'> & {
isMuted: boolean;
};
type EnhanceProps = { type EnhanceProps = {
category: CategoryModel; category: CategoryModel;
locale: string; locale: string;
@ -35,87 +29,45 @@ type EnhanceProps = {
isTablet: boolean; isTablet: boolean;
} & WithDatabaseArgs } & 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 withUserId = withObservables([], ({database}: WithDatabaseArgs) => ({currentUserId: observeCurrentUserId(database)}));
const enhance = withObservables(['category', 'isTablet', 'locale'], ({category, locale, isTablet, database, currentUserId}: EnhanceProps) => { const observeCategoryChannels = (category: CategoryModel, myChannels: Observable<MyChannelModel[]>) => {
const dmMap = (p: PreferenceModel) => getDirectChannelName(p.name, currentUserId); 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'). const enhanced = withObservables([], ({category, currentUserId, database, isTablet, locale}: EnhanceProps) => {
observeWithColumns(['value']).pipe( const categoryMyChannels = category.myChannels.observeWithColumns(['last_post_at', 'is_unread']);
switchMap((prefs: PreferenceModel[]) => { const channelsWithMyChannel = observeCategoryChannels(category, categoryMyChannels);
const names = prefs.map(dmMap); const currentChannelId = isTablet ? observeCurrentChannelId(database) : of$('');
const channels = queryChannelsByNames(database, names).observe(); const lastUnreadId = isTablet ? observeLastUnreadChannelId(database) : of$(undefined);
return channels.pipe( const unreadsOnTop = querySidebarPreferences(database, Preferences.CHANNEL_SIDEBAR_GROUP_UNREADS).
switchMap(mapChannelIds), 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); let limit = of$(Preferences.CHANNEL_SIDEBAR_LIMIT_DMS_DEFAULT);
if (category.type === DMS_CATEGORY) { if (category.type === DMS_CATEGORY) {
limit = querySidebarPreferences(database, Preferences.CHANNEL_SIDEBAR_LIMIT_DMS). 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). const notifyPropsPerChannel = categoryMyChannels.pipe(
observeWithColumns(['value']). // eslint-disable-next-line max-nested-callbacks
pipe( switchMap((mc) => observeNotifyPropsByChannels(database, mc)),
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 unreadChannels = category.myChannels.observeWithColumns(['mentions_count', 'is_unread']); const hiddenDmPrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.DIRECT_CHANNEL_SHOW, undefined, 'false').
const notifyProps = unreadChannels.pipe(switchMap((myChannels) => observeNotifyPropsByChannels(database, myChannels))); observeWithColumns(['value']);
const unreadIds = unreadChannels.pipe( const hiddenGmPrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.GROUP_CHANNEL_SHOW, undefined, 'false').
combineLatestWith(notifyProps, lastUnreadId), observeWithColumns(['value']);
map(([my, settings, lastUnread]) => { const manuallyClosedPrefs = hiddenDmPrefs.pipe(
return my.reduce<Set<string>>((set, m) => { combineLatestWith(hiddenGmPrefs),
const isMuted = settings[m.id]?.mark_unread === 'mention'; switchMap(([dms, gms]) => of$(dms.concat(gms))),
if ((isMuted && m.mentionsCount) || (!isMuted && m.isUnread) || m.id === lastUnread) { );
set.add(m.id);
} const approxViewTimePrefs = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.CHANNEL_APPROXIMATE_VIEW_TIME, undefined).
return set; observeWithColumns(['value']);
}, new Set()); 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 { return {
limit,
sortedChannels,
unreadsOnTop,
unreadIds,
category, category,
sortedChannels,
unreadIds,
unreadsOnTop,
}; };
}); });
export default withDatabase(withUserId(enhance(CategoryBody))); export default withDatabase(withUserId(enhanced(CategoryBody)));

View file

@ -54,7 +54,7 @@ const enhanced = withObservables(['currentTeamId', 'isTablet', 'onlyUnreads'], (
const channels = myUnreadChannels.pipe(switchMap((myChannels) => observeChannelsByLastPostAt(database, myChannels))); const channels = myUnreadChannels.pipe(switchMap((myChannels) => observeChannelsByLastPostAt(database, myChannels)));
const channelsMap = channels.pipe(switchMap((cs) => of$(makeChannelsMap(cs)))); 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), combineLatestWith(channelsMap, notifyProps),
map(filterAndSortMyChannels), map(filterAndSortMyChannels),
combineLatestWith(lastUnread), combineLatestWith(lastUnread),

View file

@ -4,7 +4,7 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables'; import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs'; import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators'; import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {Permissions} from '@constants'; import {Permissions} from '@constants';
import {observePermissionForTeam} from '@queries/servers/role'; import {observePermissionForTeam} from '@queries/servers/role';
@ -25,6 +25,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const canJoinChannels = combineLatest([currentUser, team]).pipe( const canJoinChannels = combineLatest([currentUser, team]).pipe(
switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.JOIN_PUBLIC_CHANNELS, true)), switchMap(([u, t]) => observePermissionForTeam(database, t, u, Permissions.JOIN_PUBLIC_CHANNELS, true)),
distinctUntilChanged(),
); );
const canCreatePublicChannels = combineLatest([currentUser, team]).pipe( const canCreatePublicChannels = combineLatest([currentUser, team]).pipe(
@ -37,6 +38,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const canCreateChannels = combineLatest([canCreatePublicChannels, canCreatePrivateChannels]).pipe( const canCreateChannels = combineLatest([canCreatePublicChannels, canCreatePrivateChannels]).pipe(
switchMap(([open, priv]) => of$(open || priv)), switchMap(([open, priv]) => of$(open || priv)),
distinctUntilChanged(),
); );
const canAddUserToTeam = combineLatest([currentUser, team]).pipe( const canAddUserToTeam = combineLatest([currentUser, team]).pipe(
@ -48,9 +50,11 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
canJoinChannels, canJoinChannels,
canInvitePeople: combineLatest([enableOpenServer, canAddUserToTeam]).pipe( canInvitePeople: combineLatest([enableOpenServer, canAddUserToTeam]).pipe(
switchMap(([openServer, addUser]) => of$(openServer && addUser)), switchMap(([openServer, addUser]) => of$(openServer && addUser)),
distinctUntilChanged(),
), ),
displayName: team.pipe( displayName: team.pipe(
switchMap((t) => of$(t?.displayName)), switchMap((t) => of$(t?.displayName)),
distinctUntilChanged(),
), ),
pushProxyStatus: observePushVerificationStatus(database), pushProxyStatus: observePushVerificationStatus(database),
}; };

View file

@ -33,8 +33,8 @@ describe('components/categories_list', () => {
it('should render', () => { it('should render', () => {
const wrapper = renderWithEverything( const wrapper = renderWithEverything(
<CategoriesList <CategoriesList
teamsCount={1} moreThanOneTeam={false}
channelsCount={1} hasChannels={true}
/>, />,
{database}, {database},
); );
@ -46,8 +46,8 @@ describe('components/categories_list', () => {
const wrapper = renderWithEverything( const wrapper = renderWithEverything(
<CategoriesList <CategoriesList
isCRTEnabled={true} isCRTEnabled={true}
teamsCount={1} moreThanOneTeam={false}
channelsCount={1} hasChannels={true}
/>, />,
{database}, {database},
); );
@ -67,8 +67,8 @@ describe('components/categories_list', () => {
jest.useFakeTimers(); jest.useFakeTimers();
const wrapper = renderWithEverything( const wrapper = renderWithEverything(
<CategoriesList <CategoriesList
teamsCount={0} moreThanOneTeam={false}
channelsCount={1} hasChannels={true}
/>, />,
{database}, {database},
); );
@ -89,8 +89,8 @@ describe('components/categories_list', () => {
jest.useFakeTimers(); jest.useFakeTimers();
const wrapper = renderWithEverything( const wrapper = renderWithEverything(
<CategoriesList <CategoriesList
teamsCount={1} moreThanOneTeam={true}
channelsCount={0} hasChannels={false}
/>, />,
{database}, {database},
); );

View file

@ -27,28 +27,28 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
})); }));
type ChannelListProps = { type ChannelListProps = {
channelsCount: number; hasChannels: boolean;
iconPad?: boolean; iconPad?: boolean;
isCRTEnabled?: boolean; isCRTEnabled?: boolean;
teamsCount: number; moreThanOneTeam: boolean;
}; };
const getTabletWidth = (teamsCount: number) => { const getTabletWidth = (moreThanOneTeam: boolean) => {
return TABLET_SIDEBAR_WIDTH - (teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0); 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 theme = useTheme();
const styles = getStyleSheet(theme); const styles = getStyleSheet(theme);
const {width} = useWindowDimensions(); const {width} = useWindowDimensions();
const isTablet = useIsTablet(); const isTablet = useIsTablet();
const tabletWidth = useSharedValue(isTablet ? getTabletWidth(teamsCount) : 0); const tabletWidth = useSharedValue(isTablet ? getTabletWidth(moreThanOneTeam) : 0);
useEffect(() => { useEffect(() => {
if (isTablet) { if (isTablet) {
tabletWidth.value = getTabletWidth(teamsCount); tabletWidth.value = getTabletWidth(moreThanOneTeam);
} }
}, [isTablet && teamsCount]); }, [isTablet && moreThanOneTeam]);
const tabletStyle = useAnimatedStyle(() => { const tabletStyle = useAnimatedStyle(() => {
if (!isTablet) { if (!isTablet) {
@ -61,7 +61,7 @@ const CategoriesList = ({channelsCount, iconPad, isCRTEnabled, teamsCount}: Chan
}, [isTablet, width]); }, [isTablet, width]);
const content = useMemo(() => { const content = useMemo(() => {
if (channelsCount < 1) { if (!hasChannels) {
return (<LoadChannelsError/>); return (<LoadChannelsError/>);
} }

View file

@ -29,9 +29,10 @@ import Servers from './servers';
import type {LaunchType} from '@typings/launch'; import type {LaunchType} from '@typings/launch';
type ChannelProps = { type ChannelProps = {
channelsCount: number; hasChannels: boolean;
isCRTEnabled: boolean; isCRTEnabled: boolean;
teamsCount: number; hasTeams: boolean;
hasMoreThanOneTeam: boolean;
isLicensed: boolean; isLicensed: boolean;
showToS: boolean; showToS: boolean;
launchType: LaunchType; launchType: LaunchType;
@ -126,10 +127,10 @@ const ChannelListScreen = (props: ChannelProps) => {
}, [theme, insets.top]); }, [theme, insets.top]);
useEffect(() => { useEffect(() => {
if (!props.teamsCount) { if (!props.hasTeams) {
resetToTeams(); resetToTeams();
} }
}, [Boolean(props.teamsCount)]); }, [Boolean(props.hasTeams)]);
useEffect(() => { useEffect(() => {
const back = BackHandler.addEventListener('hardwareBackPress', handleBackPress); const back = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
@ -176,13 +177,13 @@ const ChannelListScreen = (props: ChannelProps) => {
> >
<TeamSidebar <TeamSidebar
iconPad={canAddOtherServers} iconPad={canAddOtherServers}
teamsCount={props.teamsCount} hasMoreThanOneTeam={props.hasMoreThanOneTeam}
/> />
<CategoriesList <CategoriesList
iconPad={canAddOtherServers && props.teamsCount <= 1} iconPad={canAddOtherServers && !props.hasMoreThanOneTeam}
isCRTEnabled={props.isCRTEnabled} isCRTEnabled={props.isCRTEnabled}
teamsCount={props.teamsCount} moreThanOneTeam={props.hasMoreThanOneTeam}
channelsCount={props.channelsCount} hasChannels={props.hasChannels}
/> />
{isTablet && {isTablet &&
<AdditionalTabletView/> <AdditionalTabletView/>

View file

@ -4,7 +4,7 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables'; import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs'; import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators'; import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {queryAllMyChannelsForTeam} from '@queries/servers/channel'; import {queryAllMyChannelsForTeam} from '@queries/servers/channel';
import {observeCurrentTeamId, observeLicense} from '@queries/servers/system'; 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))), switchMap((lcs) => (lcs ? of$(lcs.IsLicensed === 'true') : of$(false))),
); );
const teamsCount = queryMyTeams(database).observeCount(false);
return { return {
isCRTEnabled: observeIsCRTEnabled(database), isCRTEnabled: observeIsCRTEnabled(database),
teamsCount: queryMyTeams(database).observeCount(false), hasTeams: teamsCount.pipe(
channelsCount: observeCurrentTeamId(database).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((id) => (id ? queryAllMyChannelsForTeam(database, id).observeCount(false) : of$(0))),
switchMap((v) => of$(v > 0)),
distinctUntilChanged(),
), ),
isLicensed, isLicensed,
showToS: observeShowToS(database), showToS: observeShowToS(database),

View file

@ -5,6 +5,7 @@ import {useIsFocused, useNavigation} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import {FlatList, LayoutChangeEvent, Platform, StyleSheet, ViewStyle} from 'react-native'; 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 Animated, {useAnimatedStyle, useDerivedValue, withTiming} from 'react-native-reanimated';
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; 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 Loading from '@components/loading';
import NavigationHeader from '@components/navigation_header'; import NavigationHeader from '@components/navigation_header';
import RoundedHeaderContext from '@components/rounded_header_context'; import RoundedHeaderContext from '@components/rounded_header_context';
import {Screens} from '@constants';
import {BOTTOM_TAB_HEIGHT} from '@constants/view'; import {BOTTOM_TAB_HEIGHT} from '@constants/view';
import {useServerUrl} from '@context/server'; import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme'; import {useTheme} from '@context/theme';
import {useKeyboardHeight} from '@hooks/device'; import {useKeyboardHeight} from '@hooks/device';
import useDidUpdate from '@hooks/did_update'; import useDidUpdate from '@hooks/did_update';
import {useCollapsibleHeader} from '@hooks/header'; import {useCollapsibleHeader} from '@hooks/header';
import NavigationStore from '@store/navigation_store';
import {FileFilter, FileFilters, filterFileExtensions} from '@utils/file'; import {FileFilter, FileFilters, filterFileExtensions} from '@utils/file';
import {TabTypes, TabType} from '@utils/search'; import {TabTypes, TabType} from '@utils/search';
@ -317,6 +320,19 @@ const SearchScreen = ({teamId, teams}: Props) => {
} }
}, [isFocused]); }, [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 ( return (
<FreezeScreen freeze={!isFocused}> <FreezeScreen freeze={!isFocused}>
<NavigationHeader <NavigationHeader

View file

@ -31,12 +31,13 @@ import type ChannelModel from '@typings/database/models/servers/channel';
import type PostModel from '@typings/database/models/servers/post'; import type PostModel from '@typings/database/models/servers/post';
import type ReactionModel from '@typings/database/models/servers/reaction'; import type ReactionModel from '@typings/database/models/servers/reaction';
import type UserModel from '@typings/database/models/servers/user'; import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type EnhancedProps = WithDatabaseArgs & { type EnhancedProps = WithDatabaseArgs & {
combinedPost?: Post | PostModel; combinedPost?: Post | PostModel;
post: PostModel; post: PostModel;
showAddReaction: boolean; showAddReaction: boolean;
location: string; sourceScreen: AvailableScreens;
serverUrl: string; 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 channel = observeChannel(database, post.channelId);
const channelIsArchived = channel.pipe(switchMap((ch: ChannelModel) => of$(ch.deleteAt !== 0))); const channelIsArchived = channel.pipe(switchMap((ch: ChannelModel) => of$(ch.deleteAt !== 0)));
const currentUser = observeCurrentUser(database); 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]) => { 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]) => { const canPin = combineLatest([channelIsArchived, channelIsReadOnly]).pipe(switchMap(([isArchived, isReadOnly]) => {

View file

@ -1,6 +1,200 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // 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) { export function makeCategoryChannelId(teamId: string, channelId: string) {
return `${teamId}_${channelId}`; 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[cwm.channel.id], lastUnreadId)) {
result.add(cwm.channel.id);
}
return result;
}, new Set());
};

View file

@ -3,10 +3,10 @@
import {isMinimumServerVersion} from './helpers'; import {isMinimumServerVersion} from './helpers';
export function hasReliableWebsocket(config: ClientConfig) { export function hasReliableWebsocket(version?: string, reliableWebsocketsConfig?: string) {
if (isMinimumServerVersion(config.Version, 6, 5)) { if (version && isMinimumServerVersion(version, 6, 5)) {
return true; return true;
} }
return config.EnableReliableWebSockets === 'true'; return reliableWebsocketsConfig === 'true';
} }

View file

@ -200,7 +200,7 @@ export function doesMatchNamedEmoji(emojiName: string) {
return false; return false;
} }
export const getEmojiFirstAlias = (emoji: string) => { export const getEmojiFirstAlias = (emoji: string): string => {
return getEmojiByName(emoji, [])?.short_names?.[0] || emoji; return getEmojiByName(emoji, [])?.short_names?.[0] || emoji;
}; };

View file

@ -1128,7 +1128,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 459; CURRENT_PROJECT_VERSION = 462;
DEVELOPMENT_TEAM = UQ8HT4Q2XM; DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
@ -1172,7 +1172,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 459; CURRENT_PROJECT_VERSION = 462;
DEVELOPMENT_TEAM = UQ8HT4Q2XM; DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
@ -1315,7 +1315,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 459; CURRENT_PROJECT_VERSION = 462;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UQ8HT4Q2XM; DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@ -1366,7 +1366,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 459; CURRENT_PROJECT_VERSION = 462;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UQ8HT4Q2XM; DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;

View file

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.1.0</string> <string>2.2.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -37,7 +37,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>459</string> <string>462</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View file

@ -19,9 +19,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.1.0</string> <string>2.2.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>459</string> <string>462</string>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>OpenSans-Bold.ttf</string> <string>OpenSans-Bold.ttf</string>

View file

@ -19,9 +19,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.1.0</string> <string>2.2.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>459</string> <string>462</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>

View file

@ -83,8 +83,9 @@ class NotificationService: UNNotificationServiceExtension {
let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false
let rootId = notification.userInfo["root_id"] as? String ?? "" let rootId = notification.userInfo["root_id"] as? String ?? ""
let channelName = notification.userInfo["channel_name"] as? String ?? "" let senderName = notification.userInfo["sender_name"] as? String
let message = (notification.userInfo["message"] 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 overrideUsername = notification.userInfo["override_username"] as? String
let senderId = notification.userInfo["sender_id"] as? String let senderId = notification.userInfo["sender_id"] as? String
let senderIdentifier = overrideUsername ?? senderId let senderIdentifier = overrideUsername ?? senderId
@ -95,10 +96,17 @@ class NotificationService: UNNotificationServiceExtension {
conversationId = rootId 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 handle = INPersonHandle(value: senderIdentifier, type: .unknown)
let sender = INPerson(personHandle: handle, let sender = INPerson(personHandle: handle,
nameComponents: nil, nameComponents: nil,
displayName: channelName, displayName: channelName ?? senderName,
image: avatar, image: avatar,
contactIdentifier: nil, contactIdentifier: nil,
customIdentifier: nil) customIdentifier: nil)

View file

@ -54,7 +54,7 @@ target 'Mattermost' do
pod 'simdjson', path: '../node_modules/@nozbe/simdjson' pod 'simdjson', path: '../node_modules/@nozbe/simdjson'
# TODO: Remove this once upstream PR https://github.com/daltoniam/Starscream/pull/871 is merged # 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 end

View file

@ -385,7 +385,7 @@ PODS:
- React-Core - React-Core
- react-native-netinfo (9.3.7): - react-native-netinfo (9.3.7):
- React-Core - React-Core
- react-native-network-client (1.3.1): - react-native-network-client (1.3.2):
- Alamofire (~> 5.6.4) - Alamofire (~> 5.6.4)
- React-Core - React-Core
- Starscream (~> 4.0.4) - Starscream (~> 4.0.4)
@ -709,7 +709,7 @@ DEPENDENCIES:
- RNSVG (from `../node_modules/react-native-svg`) - RNSVG (from `../node_modules/react-native-svg`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- "simdjson (from `../node_modules/@nozbe/simdjson`)" - "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`)" - "WatermelonDB (from `../node_modules/@nozbe/watermelondb`)"
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@ -908,7 +908,7 @@ EXTERNAL SOURCES:
simdjson: simdjson:
:path: "../node_modules/@nozbe/simdjson" :path: "../node_modules/@nozbe/simdjson"
Starscream: Starscream:
:commit: 2770c931b2758f26e29b937d547a23122e9c6583 :commit: 9575b6781d1262247096af73617ae3acb2b139a0
:git: https://github.com/mattermost/Starscream.git :git: https://github.com/mattermost/Starscream.git
WatermelonDB: WatermelonDB:
:path: "../node_modules/@nozbe/watermelondb" :path: "../node_modules/@nozbe/watermelondb"
@ -917,7 +917,7 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS: CHECKOUT OPTIONS:
Starscream: Starscream:
:commit: 2770c931b2758f26e29b937d547a23122e9c6583 :commit: 9575b6781d1262247096af73617ae3acb2b139a0
:git: https://github.com/mattermost/Starscream.git :git: https://github.com/mattermost/Starscream.git
SPEC CHECKSUMS: SPEC CHECKSUMS:
@ -977,7 +977,7 @@ SPEC CHECKSUMS:
react-native-image-picker: a5dddebb4d2955ac4712a4ed66b00a85f62a63ac react-native-image-picker: a5dddebb4d2955ac4712a4ed66b00a85f62a63ac
react-native-in-app-review: a073f67c5f3392af6ea7fb383217cdb1aa2aa726 react-native-in-app-review: a073f67c5f3392af6ea7fb383217cdb1aa2aa726
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
react-native-network-client: 116ec02566020bff98cddd9c4825e7665306ad6c react-native-network-client: 08bf5a8ad300192768ffa2c6fc929d2bba9a27aa
react-native-notifications: 83b4fd4a127a6c918fc846cae90da60f84819e44 react-native-notifications: 83b4fd4a127a6c918fc846cae90da60f84819e44
react-native-paste-input: 3392800944a47c00dddbff23c31c281482209679 react-native-paste-input: 3392800944a47c00dddbff23c31c281482209679
react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc
@ -1033,6 +1033,6 @@ SPEC CHECKSUMS:
Yoga: 5ed1699acbba8863755998a4245daa200ff3817b Yoga: 5ed1699acbba8863755998a4245daa200ff3817b
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
PODFILE CHECKSUM: 9f76739ed16bbdc0f4b1049ecb366fb5d23a0f3a PODFILE CHECKSUM: 831b649321a4d14a86a074af619aa779ebc048c4
COCOAPODS: 1.11.3 COCOAPODS: 1.11.3

157
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "mattermost-mobile", "name": "mattermost-mobile",
"version": "2.1.0", "version": "2.2.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
@ -19,7 +19,7 @@
"@gorhom/bottom-sheet": "4.4.5", "@gorhom/bottom-sheet": "4.4.5",
"@mattermost/compass-icons": "0.1.35", "@mattermost/compass-icons": "0.1.35",
"@mattermost/react-native-emm": "1.3.5", "@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-paste-input": "0.6.2",
"@mattermost/react-native-turbo-log": "0.2.3", "@mattermost/react-native-turbo-log": "0.2.3",
"@mattermost/react-native-turbo-mailer": "0.2.4", "@mattermost/react-native-turbo-mailer": "0.2.4",
@ -3241,9 +3241,9 @@
} }
}, },
"node_modules/@mattermost/react-native-network-client": { "node_modules/@mattermost/react-native-network-client": {
"version": "1.3.1", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.2.tgz",
"integrity": "sha512-DtwVLV/NUE6MkXOlVZG+4QJXou6nHMdmsxnP1+RqhOeSw5jJlQvxmQgxzxvxLpaWOag+wgB1zpDulGNbr/Cz6Q==", "integrity": "sha512-3GFNzMXZWlIXXDYQLIJlKRf+HUZKP0F7mpZ1rSTgoTmUeFdqde4uRiU/L96COg34rAdeFRFrgpk0DxEnT7NiVg==",
"dependencies": { "dependencies": {
"validator": "13.9.0", "validator": "13.9.0",
"zod": "3.20.6" "zod": "3.20.6"
@ -24294,12 +24294,13 @@
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-emm/-/react-native-emm-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@mattermost/react-native-emm/-/react-native-emm-1.3.5.tgz",
"integrity": "sha512-REdUEsm/RA6lI1Rt4b009jvWn28f7H+e27gd4hlNk6zesIh/dlfiHwYfInW/vwbNFBdSPpvHy7Qi2mdcvrNqhg==", "integrity": "sha512-REdUEsm/RA6lI1Rt4b009jvWn28f7H+e27gd4hlNk6zesIh/dlfiHwYfInW/vwbNFBdSPpvHy7Qi2mdcvrNqhg==",
"requires": {} "requires": {
}
}, },
"@mattermost/react-native-network-client": { "@mattermost/react-native-network-client": {
"version": "1.3.1", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.3.2.tgz",
"integrity": "sha512-DtwVLV/NUE6MkXOlVZG+4QJXou6nHMdmsxnP1+RqhOeSw5jJlQvxmQgxzxvxLpaWOag+wgB1zpDulGNbr/Cz6Q==", "integrity": "sha512-3GFNzMXZWlIXXDYQLIJlKRf+HUZKP0F7mpZ1rSTgoTmUeFdqde4uRiU/L96COg34rAdeFRFrgpk0DxEnT7NiVg==",
"requires": { "requires": {
"validator": "13.9.0", "validator": "13.9.0",
"zod": "3.20.6" "zod": "3.20.6"
@ -24317,13 +24318,15 @@
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-turbo-log/-/react-native-turbo-log-0.2.3.tgz", "resolved": "https://registry.npmjs.org/@mattermost/react-native-turbo-log/-/react-native-turbo-log-0.2.3.tgz",
"integrity": "sha512-usWyD8zVAHzrYqgPH1ne5I14gCOkhS2mefK58g5v4DewZfCm0/Uc0w8MRuPS/9jyOPPq1rUZj8U1AqKgEne9tQ==", "integrity": "sha512-usWyD8zVAHzrYqgPH1ne5I14gCOkhS2mefK58g5v4DewZfCm0/Uc0w8MRuPS/9jyOPPq1rUZj8U1AqKgEne9tQ==",
"requires": {} "requires": {
}
}, },
"@mattermost/react-native-turbo-mailer": { "@mattermost/react-native-turbo-mailer": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-turbo-mailer/-/react-native-turbo-mailer-0.2.4.tgz", "resolved": "https://registry.npmjs.org/@mattermost/react-native-turbo-mailer/-/react-native-turbo-mailer-0.2.4.tgz",
"integrity": "sha512-6W37UvLxg7vhP5YJXZfzKvPy4r9bozqSSuB4gbC2EjvWrGB4LfwKWljgw+Gb/E8x3ceMCib2SPPMz+thzs7DHw==", "integrity": "sha512-6W37UvLxg7vhP5YJXZfzKvPy4r9bozqSSuB4gbC2EjvWrGB4LfwKWljgw+Gb/E8x3ceMCib2SPPMz+thzs7DHw==",
"requires": {} "requires": {
}
}, },
"@msgpack/msgpack": { "@msgpack/msgpack": {
"version": "2.8.0", "version": "2.8.0",
@ -24409,13 +24412,15 @@
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@react-native-camera-roll/camera-roll/-/camera-roll-5.2.3.tgz", "resolved": "https://registry.npmjs.org/@react-native-camera-roll/camera-roll/-/camera-roll-5.2.3.tgz",
"integrity": "sha512-GNdFJj5F2pPQw6RH/BxbT0Ner+WPCm6olmjLn8JBlaRQ0IBX4K9l44YKJLJZsMJ54uqiKK0fDiRzT90Eg0iuIg==", "integrity": "sha512-GNdFJj5F2pPQw6RH/BxbT0Ner+WPCm6olmjLn8JBlaRQ0IBX4K9l44YKJLJZsMJ54uqiKK0fDiRzT90Eg0iuIg==",
"requires": {} "requires": {
}
}, },
"@react-native-clipboard/clipboard": { "@react-native-clipboard/clipboard": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.11.1.tgz",
"integrity": "sha512-nvSIIHzybVWqYxcJE5hpT17ekxAAg383Ggzw5WrYHtkKX61N1AwaKSNmXs5xHV7pmKSOe/yWjtSwxIzfW51I5Q==", "integrity": "sha512-nvSIIHzybVWqYxcJE5hpT17ekxAAg383Ggzw5WrYHtkKX61N1AwaKSNmXs5xHV7pmKSOe/yWjtSwxIzfW51I5Q==",
"requires": {} "requires": {
}
}, },
"@react-native-community/cli": { "@react-native-community/cli": {
"version": "10.1.3", "version": "10.1.3",
@ -25571,7 +25576,8 @@
"version": "9.3.7", "version": "9.3.7",
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-9.3.7.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-9.3.7.tgz",
"integrity": "sha512-+taWmE5WpBp0uS6kf+bouCx/sn89G9EpR4s2M/ReLvctVIFL2Qh8WnWfBxqK9qwgmFha/uqjSr2Gq03OOtiDcw==", "integrity": "sha512-+taWmE5WpBp0uS6kf+bouCx/sn89G9EpR4s2M/ReLvctVIFL2Qh8WnWfBxqK9qwgmFha/uqjSr2Gq03OOtiDcw==",
"requires": {} "requires": {
}
}, },
"@react-native-cookies/cookies": { "@react-native-cookies/cookies": {
"version": "6.2.1", "version": "6.2.1",
@ -25647,7 +25653,8 @@
"version": "1.3.15", "version": "1.3.15",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.15.tgz", "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.15.tgz",
"integrity": "sha512-CR4CEYJVY0OLyeLQi9N3Z2o4K47gXctvFxfZizDuW1xFtCJbA0eGvpjSLXEWHoY0hFjrlC6KinpdepGHVxhYIg==", "integrity": "sha512-CR4CEYJVY0OLyeLQi9N3Z2o4K47gXctvFxfZizDuW1xFtCJbA0eGvpjSLXEWHoY0hFjrlC6KinpdepGHVxhYIg==",
"requires": {} "requires": {
}
}, },
"@react-navigation/native": { "@react-navigation/native": {
"version": "6.1.4", "version": "6.1.4",
@ -25952,63 +25959,72 @@
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@stream-io/flat-list-mvcp/-/flat-list-mvcp-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@stream-io/flat-list-mvcp/-/flat-list-mvcp-0.10.2.tgz",
"integrity": "sha512-jebEKP7pfRF8/tVSqNM6qdvisfOtMnMlzGYTWldoOnIq9/6DS1BU4ilzBuH6O7iBUu4bDokrMCNJgA2b2EKW/A==", "integrity": "sha512-jebEKP7pfRF8/tVSqNM6qdvisfOtMnMlzGYTWldoOnIq9/6DS1BU4ilzBuH6O7iBUu4bDokrMCNJgA2b2EKW/A==",
"requires": {} "requires": {
}
}, },
"@svgr/babel-plugin-add-jsx-attribute": { "@svgr/babel-plugin-add-jsx-attribute": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.0.0.tgz",
"integrity": "sha512-MdPdhdWLtQsjd29Wa4pABdhWbaRMACdM1h31BY+c6FghTZqNGT7pEYdBoaGeKtdTOBC/XNFQaKVj+r/Ei2ryWA==", "integrity": "sha512-MdPdhdWLtQsjd29Wa4pABdhWbaRMACdM1h31BY+c6FghTZqNGT7pEYdBoaGeKtdTOBC/XNFQaKVj+r/Ei2ryWA==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@svgr/babel-plugin-remove-jsx-attribute": { "@svgr/babel-plugin-remove-jsx-attribute": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.0.0.tgz",
"integrity": "sha512-aVdtfx9jlaaxc3unA6l+M9YRnKIZjOhQPthLKqmTXC8UVkBLDRGwPKo+r8n3VZN8B34+yVajzPTZ+ptTSuZZCw==", "integrity": "sha512-aVdtfx9jlaaxc3unA6l+M9YRnKIZjOhQPthLKqmTXC8UVkBLDRGwPKo+r8n3VZN8B34+yVajzPTZ+ptTSuZZCw==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@svgr/babel-plugin-remove-jsx-empty-expression": { "@svgr/babel-plugin-remove-jsx-empty-expression": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.0.0.tgz",
"integrity": "sha512-Ccj42ApsePD451AZJJf1QzTD1B/BOU392URJTeXFxSK709i0KUsGtbwyiqsKu7vsYxpTM0IA5clAKDyf9RCZyA==", "integrity": "sha512-Ccj42ApsePD451AZJJf1QzTD1B/BOU392URJTeXFxSK709i0KUsGtbwyiqsKu7vsYxpTM0IA5clAKDyf9RCZyA==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@svgr/babel-plugin-replace-jsx-attribute-value": { "@svgr/babel-plugin-replace-jsx-attribute-value": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.0.0.tgz",
"integrity": "sha512-88V26WGyt1Sfd1emBYmBJRWMmgarrExpKNVmI9vVozha4kqs6FzQJ/Kp5+EYli1apgX44518/0+t9+NU36lThQ==", "integrity": "sha512-88V26WGyt1Sfd1emBYmBJRWMmgarrExpKNVmI9vVozha4kqs6FzQJ/Kp5+EYli1apgX44518/0+t9+NU36lThQ==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@svgr/babel-plugin-svg-dynamic-title": { "@svgr/babel-plugin-svg-dynamic-title": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.0.0.tgz",
"integrity": "sha512-F7YXNLfGze+xv0KMQxrl2vkNbI9kzT9oDK55/kUuymh1ACyXkMV+VZWX1zEhSTfEKh7VkHVZGmVtHg8eTZ6PRg==", "integrity": "sha512-F7YXNLfGze+xv0KMQxrl2vkNbI9kzT9oDK55/kUuymh1ACyXkMV+VZWX1zEhSTfEKh7VkHVZGmVtHg8eTZ6PRg==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@svgr/babel-plugin-svg-em-dimensions": { "@svgr/babel-plugin-svg-em-dimensions": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.0.0.tgz",
"integrity": "sha512-+rghFXxdIqJNLQK08kwPBD3Z22/0b2tEZ9lKiL/yTfuyj1wW8HUXu4bo/XkogATIYuXSghVQOOCwURXzHGKyZA==", "integrity": "sha512-+rghFXxdIqJNLQK08kwPBD3Z22/0b2tEZ9lKiL/yTfuyj1wW8HUXu4bo/XkogATIYuXSghVQOOCwURXzHGKyZA==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@svgr/babel-plugin-transform-react-native-svg": { "@svgr/babel-plugin-transform-react-native-svg": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.0.0.tgz",
"integrity": "sha512-VaphyHZ+xIKv5v0K0HCzyfAaLhPGJXSk2HkpYfXIOKb7DjLBv0soHDxNv6X0vr2titsxE7klb++u7iOf7TSrFQ==", "integrity": "sha512-VaphyHZ+xIKv5v0K0HCzyfAaLhPGJXSk2HkpYfXIOKb7DjLBv0soHDxNv6X0vr2titsxE7klb++u7iOf7TSrFQ==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@svgr/babel-plugin-transform-svg-component": { "@svgr/babel-plugin-transform-svg-component": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.1.0.tgz",
"integrity": "sha512-1zacrn08K5RyV2NtXahOZ5Im/+aB1Y0LVh6QpzwgQV05sY7H5Npq+OcW/UqXbfB2Ua/WnHsFossFQqigCjarYg==", "integrity": "sha512-1zacrn08K5RyV2NtXahOZ5Im/+aB1Y0LVh6QpzwgQV05sY7H5Npq+OcW/UqXbfB2Ua/WnHsFossFQqigCjarYg==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@svgr/babel-preset": { "@svgr/babel-preset": {
"version": "6.1.0", "version": "6.1.0",
@ -26926,7 +26942,8 @@
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz",
"integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@webpack-cli/info": { "@webpack-cli/info": {
"version": "1.4.0", "version": "1.4.0",
@ -26942,7 +26959,8 @@
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz",
"integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"@xtuc/ieee754": { "@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
@ -26997,14 +27015,16 @@
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
"dev": true, "dev": true,
"peer": true, "peer": true,
"requires": {} "requires": {
}
}, },
"acorn-jsx": { "acorn-jsx": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"adm-zip": { "adm-zip": {
"version": "0.5.9", "version": "0.5.9",
@ -27066,7 +27086,8 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true, "dev": true,
"peer": true, "peer": true,
"requires": {} "requires": {
}
}, },
"anser": { "anser": {
"version": "1.4.10", "version": "1.4.10",
@ -27333,7 +27354,8 @@
"version": "7.0.0-bridge.0", "version": "7.0.0-bridge.0",
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz",
"integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==",
"requires": {} "requires": {
}
}, },
"babel-jest": { "babel-jest": {
"version": "29.4.3", "version": "29.4.3",
@ -29311,7 +29333,8 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"eslint-import-resolver-node": { "eslint-import-resolver-node": {
"version": "0.3.7", "version": "0.3.7",
@ -29399,7 +29422,8 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz",
"integrity": "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==", "integrity": "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"eslint-plugin-import": { "eslint-plugin-import": {
"version": "2.27.5", "version": "2.27.5",
@ -29535,7 +29559,8 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"eslint-plugin-react-native": { "eslint-plugin-react-native": {
"version": "4.0.0", "version": "4.0.0",
@ -32097,7 +32122,8 @@
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
"integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
"dev": true, "dev": true,
"requires": {} "requires": {
}
}, },
"jest-regex-util": { "jest-regex-util": {
"version": "29.4.3", "version": "29.4.3",
@ -35550,7 +35576,8 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz",
"integrity": "sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g==", "integrity": "sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g==",
"requires": {} "requires": {
}
}, },
"react-intl": { "react-intl": {
"version": "6.2.8", "version": "6.2.8",
@ -35736,7 +35763,8 @@
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.4.1.tgz", "resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.4.1.tgz",
"integrity": "sha512-TE4Kiy7jUyv+hugxDxitzu38sW1NqjCk4uE5IgU2WevLv7sZacaBc6PZKOShNRPGirLl1NWkaG3LDEkdb9Um5g==", "integrity": "sha512-TE4Kiy7jUyv+hugxDxitzu38sW1NqjCk4uE5IgU2WevLv7sZacaBc6PZKOShNRPGirLl1NWkaG3LDEkdb9Um5g==",
"requires": {} "requires": {
}
}, },
"react-native-button": { "react-native-button": {
"version": "3.0.1", "version": "3.0.1",
@ -35777,13 +35805,15 @@
"version": "1.6.4", "version": "1.6.4",
"resolved": "https://registry.npmjs.org/react-native-create-thumbnail/-/react-native-create-thumbnail-1.6.4.tgz", "resolved": "https://registry.npmjs.org/react-native-create-thumbnail/-/react-native-create-thumbnail-1.6.4.tgz",
"integrity": "sha512-JWuKXswDXtqUPfuqh6rjCVMvTSSG3kUtwvSK/YdaNU0i+nZKxeqHmt/CO2+TyI/WSUFynGVmWT1xOHhCZAFsRQ==", "integrity": "sha512-JWuKXswDXtqUPfuqh6rjCVMvTSSG3kUtwvSK/YdaNU0i+nZKxeqHmt/CO2+TyI/WSUFynGVmWT1xOHhCZAFsRQ==",
"requires": {} "requires": {
}
}, },
"react-native-device-info": { "react-native-device-info": {
"version": "10.4.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.4.0.tgz", "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-10.4.0.tgz",
"integrity": "sha512-Z37e0HtpBvfkPRgv4xN7lXpvmJyzjwCXSFTXEkw6m2UgnnIsWlOD02Avu4hJXBlIMMazaW3ZLKal3o9h3AYvCw==", "integrity": "sha512-Z37e0HtpBvfkPRgv4xN7lXpvmJyzjwCXSFTXEkw6m2UgnnIsWlOD02Avu4hJXBlIMMazaW3ZLKal3o9h3AYvCw==",
"requires": {} "requires": {
}
}, },
"react-native-document-picker": { "react-native-document-picker": {
"version": "8.1.3", "version": "8.1.3",
@ -35820,19 +35850,22 @@
"version": "2.10.10", "version": "2.10.10",
"resolved": "https://registry.npmjs.org/react-native-exception-handler/-/react-native-exception-handler-2.10.10.tgz", "resolved": "https://registry.npmjs.org/react-native-exception-handler/-/react-native-exception-handler-2.10.10.tgz",
"integrity": "sha512-otAXGoZDl1689OoUJWN/rXxVbdoZ3xcmyF1uq/CsizdLwwyZqVGd6d+p/vbYvnF996FfEyAEBnHrdFxulTn51w==", "integrity": "sha512-otAXGoZDl1689OoUJWN/rXxVbdoZ3xcmyF1uq/CsizdLwwyZqVGd6d+p/vbYvnF996FfEyAEBnHrdFxulTn51w==",
"requires": {} "requires": {
}
}, },
"react-native-fast-image": { "react-native-fast-image": {
"version": "8.6.3", "version": "8.6.3",
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz", "resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz",
"integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==", "integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==",
"requires": {} "requires": {
}
}, },
"react-native-file-viewer": { "react-native-file-viewer": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/react-native-file-viewer/-/react-native-file-viewer-2.1.5.tgz", "resolved": "https://registry.npmjs.org/react-native-file-viewer/-/react-native-file-viewer-2.1.5.tgz",
"integrity": "sha512-MGC6sx9jsqHdefhVQ6o0akdsPGpkXgiIbpygb2Sg4g4bh7v6K1cardLV1NwGB9A6u1yICOSDT/MOC//9Ez6EUg==", "integrity": "sha512-MGC6sx9jsqHdefhVQ6o0akdsPGpkXgiIbpygb2Sg4g4bh7v6K1cardLV1NwGB9A6u1yICOSDT/MOC//9Ez6EUg==",
"requires": {} "requires": {
}
}, },
"react-native-fs": { "react-native-fs": {
"version": "2.20.0", "version": "2.20.0",
@ -35871,19 +35904,22 @@
"version": "1.14.0", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-1.14.0.tgz", "resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-1.14.0.tgz",
"integrity": "sha512-dSXZ6gAzl+W/L7BPjOpnT0bx0cgQiSr0sB3DjyDJbGIdVr4ISaktZC6gC9xYFTv2kMq0+KtbKi+dpd0WtxYZMw==", "integrity": "sha512-dSXZ6gAzl+W/L7BPjOpnT0bx0cgQiSr0sB3DjyDJbGIdVr4ISaktZC6gC9xYFTv2kMq0+KtbKi+dpd0WtxYZMw==",
"requires": {} "requires": {
}
}, },
"react-native-hw-keyboard-event": { "react-native-hw-keyboard-event": {
"version": "0.0.4", "version": "0.0.4",
"resolved": "https://registry.npmjs.org/react-native-hw-keyboard-event/-/react-native-hw-keyboard-event-0.0.4.tgz", "resolved": "https://registry.npmjs.org/react-native-hw-keyboard-event/-/react-native-hw-keyboard-event-0.0.4.tgz",
"integrity": "sha512-G8qp0nm17PHigLb/axgdF9xg51BKCG2p1AGeq//J/luLp5zNczIcQJh+nm02R1MeEUE3e53wqO4LMe0MV3raZg==", "integrity": "sha512-G8qp0nm17PHigLb/axgdF9xg51BKCG2p1AGeq//J/luLp5zNczIcQJh+nm02R1MeEUE3e53wqO4LMe0MV3raZg==",
"requires": {} "requires": {
}
}, },
"react-native-image-picker": { "react-native-image-picker": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-5.0.2.tgz", "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-5.0.2.tgz",
"integrity": "sha512-kl5g22TEiOkHcwKFh+BjdBYb+7LTq0yKXZCjgXCPoHbq7QwckgR4mFASPPoX2RUKGFy6cYqJGc0IX+xpqcSX5w==", "integrity": "sha512-kl5g22TEiOkHcwKFh+BjdBYb+7LTq0yKXZCjgXCPoHbq7QwckgR4mFASPPoX2RUKGFy6cYqJGc0IX+xpqcSX5w==",
"requires": {} "requires": {
}
}, },
"react-native-in-app-review": { "react-native-in-app-review": {
"version": "4.2.1", "version": "4.2.1",
@ -35893,13 +35929,15 @@
"react-native-incall-manager": { "react-native-incall-manager": {
"version": "git+ssh://git@github.com/cpoile/react-native-incall-manager.git#6b66ae7bab194c82573c7b3891b0ac3af71d424e", "version": "git+ssh://git@github.com/cpoile/react-native-incall-manager.git#6b66ae7bab194c82573c7b3891b0ac3af71d424e",
"from": "react-native-incall-manager@github:cpoile/react-native-incall-manager", "from": "react-native-incall-manager@github:cpoile/react-native-incall-manager",
"requires": {} "requires": {
}
}, },
"react-native-iphone-x-helper": { "react-native-iphone-x-helper": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz", "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
"integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==", "integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==",
"requires": {} "requires": {
}
}, },
"react-native-keyboard-aware-scroll-view": { "react-native-keyboard-aware-scroll-view": {
"version": "0.9.5", "version": "0.9.5",
@ -35914,7 +35952,8 @@
"version": "5.7.0", "version": "5.7.0",
"resolved": "https://registry.npmjs.org/react-native-keyboard-tracking-view/-/react-native-keyboard-tracking-view-5.7.0.tgz", "resolved": "https://registry.npmjs.org/react-native-keyboard-tracking-view/-/react-native-keyboard-tracking-view-5.7.0.tgz",
"integrity": "sha512-MDeEwAbn9LJDOfHq0QLCGaZirVLk2X/tHqkAqz3y6uxryTRdSl9PwleOVar5Jx2oAPEg4J9BXbUD1wwOOi+5Kg==", "integrity": "sha512-MDeEwAbn9LJDOfHq0QLCGaZirVLk2X/tHqkAqz3y6uxryTRdSl9PwleOVar5Jx2oAPEg4J9BXbUD1wwOOi+5Kg==",
"requires": {} "requires": {
}
}, },
"react-native-keychain": { "react-native-keychain": {
"version": "8.1.1", "version": "8.1.1",
@ -35925,13 +35964,15 @@
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.6.2.tgz", "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.6.2.tgz",
"integrity": "sha512-Z8Xxvupsex+9BBFoSYS87bilNPWcRfRsGC0cpJk72Nxb5p2nEkGSBv73xZbEHnW2mUFvP+huYxrVvjZkr/gRjQ==", "integrity": "sha512-Z8Xxvupsex+9BBFoSYS87bilNPWcRfRsGC0cpJk72Nxb5p2nEkGSBv73xZbEHnW2mUFvP+huYxrVvjZkr/gRjQ==",
"requires": {} "requires": {
}
}, },
"react-native-localize": { "react-native-localize": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-2.2.4.tgz", "resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-2.2.4.tgz",
"integrity": "sha512-gVmbyAEQQnBQ8vKlAQchFfIISeId3qT6Lc7LHmKF39nsYWX9KN4PHuG6Hk+7gduMI6IHKeZGKcLsOdh6wvN6cg==", "integrity": "sha512-gVmbyAEQQnBQ8vKlAQchFfIISeId3qT6Lc7LHmKF39nsYWX9KN4PHuG6Hk+7gduMI6IHKeZGKcLsOdh6wvN6cg==",
"requires": {} "requires": {
}
}, },
"react-native-math-view": { "react-native-math-view": {
"version": "3.9.5", "version": "3.9.5",
@ -35967,13 +36008,15 @@
"version": "4.3.3", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/react-native-notifications/-/react-native-notifications-4.3.3.tgz", "resolved": "https://registry.npmjs.org/react-native-notifications/-/react-native-notifications-4.3.3.tgz",
"integrity": "sha512-t7uPgpC93A4L41Jea2zet3BUqgh45gpN/ATS9gxejNH3r6kWgMdGeeZJhuMpl1gSXw1gcvgzdjzIjN7YZSOP0A==", "integrity": "sha512-t7uPgpC93A4L41Jea2zet3BUqgh45gpN/ATS9gxejNH3r6kWgMdGeeZJhuMpl1gSXw1gcvgzdjzIjN7YZSOP0A==",
"requires": {} "requires": {
}
}, },
"react-native-permissions": { "react-native-permissions": {
"version": "3.6.1", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.6.1.tgz", "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.6.1.tgz",
"integrity": "sha512-fzPpPQXeD34inUccqtoResSwYubfrwUguP4qrVUUv8+KSMjYSaHGoS5HaIJLZHlN9gO+TvLJZ2L5ZljTsb6qnQ==", "integrity": "sha512-fzPpPQXeD34inUccqtoResSwYubfrwUguP4qrVUUv8+KSMjYSaHGoS5HaIJLZHlN9gO+TvLJZ2L5ZljTsb6qnQ==",
"requires": {} "requires": {
}
}, },
"react-native-ratings": { "react-native-ratings": {
"version": "8.0.4", "version": "8.0.4",
@ -36001,7 +36044,8 @@
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.5.0.tgz", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.5.0.tgz",
"integrity": "sha512-0WORnk9SkREGUg2V7jHZbuN5x4vcxj/1B0QOcXJjdYWrzZHgLcUzYWWIUecUPJh747Mwjt/42RZDOaFn3L8kPQ==", "integrity": "sha512-0WORnk9SkREGUg2V7jHZbuN5x4vcxj/1B0QOcXJjdYWrzZHgLcUzYWWIUecUPJh747Mwjt/42RZDOaFn3L8kPQ==",
"requires": {} "requires": {
}
}, },
"react-native-screens": { "react-native-screens": {
"version": "3.20.0", "version": "3.20.0",
@ -36034,7 +36078,8 @@
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/react-native-size-matters/-/react-native-size-matters-0.3.1.tgz", "resolved": "https://registry.npmjs.org/react-native-size-matters/-/react-native-size-matters-0.3.1.tgz",
"integrity": "sha512-mKOfBLIBFBcs9br1rlZDvxD5+mAl8Gfr5CounwJtxI6Z82rGrMO+Kgl9EIg3RMVf3G855a85YVqHJL2f5EDRlw==", "integrity": "sha512-mKOfBLIBFBcs9br1rlZDvxD5+mAl8Gfr5CounwJtxI6Z82rGrMO+Kgl9EIg3RMVf3G855a85YVqHJL2f5EDRlw==",
"requires": {} "requires": {
}
}, },
"react-native-svg": { "react-native-svg": {
"version": "13.8.0", "version": "13.8.0",
@ -38079,7 +38124,8 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"requires": {} "requires": {
}
}, },
"utf8": { "utf8": {
"version": "3.0.0", "version": "3.0.0",
@ -38428,7 +38474,8 @@
"version": "7.5.5", "version": "7.5.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
"requires": {} "requires": {
}
}, },
"xcode": { "xcode": {
"version": "3.0.1", "version": "3.0.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "mattermost-mobile", "name": "mattermost-mobile",
"version": "2.1.0", "version": "2.2.0",
"description": "Mattermost Mobile with React Native", "description": "Mattermost Mobile with React Native",
"repository": "git@github.com:mattermost/mattermost-mobile.git", "repository": "git@github.com:mattermost/mattermost-mobile.git",
"author": "Mattermost, Inc.", "author": "Mattermost, Inc.",
@ -16,7 +16,7 @@
"@gorhom/bottom-sheet": "4.4.5", "@gorhom/bottom-sheet": "4.4.5",
"@mattermost/compass-icons": "0.1.35", "@mattermost/compass-icons": "0.1.35",
"@mattermost/react-native-emm": "1.3.5", "@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-paste-input": "0.6.2",
"@mattermost/react-native-turbo-log": "0.2.3", "@mattermost/react-native-turbo-log": "0.2.3",
"@mattermost/react-native-turbo-mailer": "0.2.4", "@mattermost/react-native-turbo-mailer": "0.2.4",

View file

@ -25,7 +25,7 @@ declare class CategoryModel extends Model {
displayName: string; displayName: string;
/** type : The type of category */ /** type : The type of category */
type: string; type: CategoryType;
/** sort_order : The sort order for this category */ /** sort_order : The sort order for this category */
sortOrder: number; sortOrder: number;