From 56d2d57291a5e649fbdd1a8998023ae186219949 Mon Sep 17 00:00:00 2001 From: Joseph Baylon Date: Thu, 15 Dec 2022 14:47:33 -0800 Subject: [PATCH 01/71] Detox/E2E Maintenance: Fix broken iOS e2e tests --- app/components/selected_users/index.tsx | 10 +++++++-- .../tutorial_highlight/long_press.tsx | 1 + .../tutorial_highlight/swipe_left.tsx | 1 + .../channel_info_start_button.tsx | 2 +- app/screens/channel_info/options/index.tsx | 5 ++++- .../create_direct_message.tsx | 1 + app/screens/settings/advanced/index.tsx | 2 +- .../e2e/support/ui/screen/browse_channels.ts | 3 ++- detox/e2e/support/ui/screen/channel_info.ts | 4 ++++ detox/e2e/support/ui/screen/channel_list.ts | 2 -- .../ui/screen/create_direct_message.ts | 7 +++++-- .../ui/screen/create_or_edit_channel.ts | 3 ++- detox/e2e/support/ui/screen/login.ts | 3 ++- detox/e2e/support/ui/screen/server_list.ts | 16 +++++++++----- .../test/autocomplete/create_channel.e2e.ts | 3 ++- .../e2e/test/autocomplete/edit_channel.e2e.ts | 3 ++- detox/e2e/test/channels/channel_info.e2e.ts | 6 ++++-- detox/e2e/test/channels/channel_list.e2e.ts | 9 +++++--- .../channels/create_direct_message.e2e.ts | 1 - .../favorite_and_unfavorite_channel.e2e.ts | 21 ++++++++++++------- detox/e2e/test/channels/leave_channel.e2e.ts | 2 ++ .../channels/mute_and_unmute_channel.e2e.ts | 5 +++++ .../messaging/save_and_unsave_message.e2e.ts | 3 ++- .../server_login/connect_to_server.e2e.ts | 2 ++ .../e2e/test/server_login/server_list.e2e.ts | 4 +++- detox/e2e/test/smoke_test/messaging.e2e.ts | 3 ++- detox/e2e/test/smoke_test/server_login.e2e.ts | 2 +- detox/e2e/test/smoke_test/threads.e2e.ts | 4 +++- detox/e2e/test/teams/invite_people.e2e.ts | 9 ++++---- .../mark_thread_as_read_and_unread.e2e.ts | 3 ++- 30 files changed, 98 insertions(+), 42 deletions(-) diff --git a/app/components/selected_users/index.tsx b/app/components/selected_users/index.tsx index 72a639aee6..e2ffad6f76 100644 --- a/app/components/selected_users/index.tsx +++ b/app/components/selected_users/index.tsx @@ -67,6 +67,11 @@ type Props = { */ teammateNameDisplay: string; + /** + * test ID + */ + testID?: string; + /** * toast Icon */ @@ -130,7 +135,7 @@ export default function SelectedUsers({ buttonIcon, buttonText, containerHeight = 0, modalPosition = 0, onPress, onRemove, selectedIds, setShowToast, showToast = false, - teammateNameDisplay, toastIcon, toastMessage, + teammateNameDisplay, testID, toastIcon, toastMessage, }: Props) { const theme = useTheme(); const style = getStyleFromTheme(theme); @@ -157,7 +162,7 @@ export default function SelectedUsers({ user={selectedIds[id]} teammateNameDisplay={teammateNameDisplay} onRemove={onRemove} - testID='create_direct_message.selected_user' + testID={`${testID}.selected_user`} />, ); } @@ -276,6 +281,7 @@ export default function SelectedUsers({ icon={buttonIcon} text={buttonText} disabled={numberSelectedIds > General.MAX_USERS_IN_GM} + testID={`${testID}.start.button`} /> diff --git a/app/components/tutorial_highlight/long_press.tsx b/app/components/tutorial_highlight/long_press.tsx index 561af0d256..04bae5a81d 100644 --- a/app/components/tutorial_highlight/long_press.tsx +++ b/app/components/tutorial_highlight/long_press.tsx @@ -48,6 +48,7 @@ const TutorialSwipeLeft = ({containerStyle, message, style, textStyles}: Props) diff --git a/app/components/tutorial_highlight/swipe_left.tsx b/app/components/tutorial_highlight/swipe_left.tsx index 505116f6a1..4669a6fbaf 100644 --- a/app/components/tutorial_highlight/swipe_left.tsx +++ b/app/components/tutorial_highlight/swipe_left.tsx @@ -48,6 +48,7 @@ const TutorialSwipeLeft = ({containerStyle, message, style, textStyles}: Props) diff --git a/app/products/calls/components/channel_info_start/channel_info_start_button.tsx b/app/products/calls/components/channel_info_start/channel_info_start_button.tsx index 9f28574562..6cdeb09c83 100644 --- a/app/products/calls/components/channel_info_start/channel_info_start_button.tsx +++ b/app/products/calls/components/channel_info_start/channel_info_start_button.tsx @@ -69,7 +69,7 @@ const ChannelInfoStartButton = ({ destructiveText={leaveText} destructiveIconName={'phone-hangup'} isDestructive={alreadyInCall} - testID='channel_info.options.join_start_call.option' + testID='channel_info.channel_actions.join_start_call.action' /> ); }; diff --git a/app/screens/channel_info/options/index.tsx b/app/screens/channel_info/options/index.tsx index ca7b62abc8..d2c856ec6f 100644 --- a/app/screens/channel_info/options/index.tsx +++ b/app/screens/channel_info/options/index.tsx @@ -32,7 +32,10 @@ const Options = ({channelId, type, callsEnabled}: Props) => { } {callsEnabled && !isDMorGM && // if calls is not enabled, copy link will show in the channel actions - + } {type !== General.DM_CHANNEL && type !== General.GM_CHANNEL && diff --git a/app/screens/create_direct_message/create_direct_message.tsx b/app/screens/create_direct_message/create_direct_message.tsx index 53d5b002d8..05fe20a16c 100644 --- a/app/screens/create_direct_message/create_direct_message.tsx +++ b/app/screens/create_direct_message/create_direct_message.tsx @@ -396,6 +396,7 @@ export default function CreateDirectMessage({ onPress={startConversation} buttonIcon={'forum-outline'} buttonText={formatMessage(messages.buttonText)} + testID='create_direct_message' /> ); diff --git a/app/screens/settings/advanced/index.tsx b/app/screens/settings/advanced/index.tsx index 79fc7b3a11..642067bb99 100644 --- a/app/screens/settings/advanced/index.tsx +++ b/app/screens/settings/advanced/index.tsx @@ -99,7 +99,7 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => { icon='trash-can-outline' info={getFormattedFileSize(dataSize || 0)} label={intl.formatMessage({id: 'settings.advanced.delete_data', defaultMessage: 'Delete local files'})} - testID='settings.advanced.delete_data.option' + testID='advanced_settings.delete_data.option' type='none' /> diff --git a/detox/e2e/support/ui/screen/browse_channels.ts b/detox/e2e/support/ui/screen/browse_channels.ts index 67903f635d..afa09fe759 100644 --- a/detox/e2e/support/ui/screen/browse_channels.ts +++ b/detox/e2e/support/ui/screen/browse_channels.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {ChannelListScreen} from '@support/ui/screen'; -import {timeouts} from '@support/utils'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; class BrowseChannelsScreen { @@ -50,6 +50,7 @@ class BrowseChannelsScreen { open = async () => { // # Open browse channels screen await ChannelListScreen.headerPlusButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelListScreen.browseChannelsItem.tap(); return this.toBeVisible(); diff --git a/detox/e2e/support/ui/screen/channel_info.ts b/detox/e2e/support/ui/screen/channel_info.ts index 3b927792e8..ead3b21773 100644 --- a/detox/e2e/support/ui/screen/channel_info.ts +++ b/detox/e2e/support/ui/screen/channel_info.ts @@ -25,6 +25,7 @@ class ChannelInfoScreen { setHeaderAction: 'channel_info.channel_actions.set_header.action', addPeopleAction: 'channel_info.channel_actions.add_people.action', copyChannelLinkAction: 'channel_info.channel_actions.copy_channel_link.action', + joinStartCallAction: 'channel_info.channel_actions.join_start_call.action', extraHeader: 'channel_info.extra.header', extraCreatedBy: 'channel_info.extra.created_by', extraCreatedOn: 'channel_info.extra.created_on', @@ -33,6 +34,7 @@ class ChannelInfoScreen { notificationPreferenceOption: 'channel_info.options.notification_preference.option', pinnedMessagesOption: 'channel_info.options.pinned_messages.option', membersOption: 'channel_info.options.members.option', + copyChannelLinkOption: 'channel_info.options.copy_channel_link.option', editChannelOption: 'channel_info.options.edit_channel.option', convertPrivateOption: 'channel_info.options.convert_private.option', leaveChannelOption: 'channel_info.options.leave_channel.option', @@ -53,6 +55,7 @@ class ChannelInfoScreen { setHeaderAction = element(by.id(this.testID.setHeaderAction)); addPeopleAction = element(by.id(this.testID.addPeopleAction)); copyChannelLinkAction = element(by.id(this.testID.copyChannelLinkAction)); + joinStartCallAction = element(by.id(this.testID.joinStartCallAction)); extraHeader = element(by.id(this.testID.extraHeader)); extraCreatedBy = element(by.id(this.testID.extraCreatedBy)); extraCreatedOn = element(by.id(this.testID.extraCreatedOn)); @@ -61,6 +64,7 @@ class ChannelInfoScreen { notificationPreferenceOption = element(by.id(this.testID.notificationPreferenceOption)); pinnedMessagesOption = element(by.id(this.testID.pinnedMessagesOption)); membersOption = element(by.id(this.testID.membersOption)); + copyChannelLinkOption = element(by.id(this.testID.copyChannelLinkOption)); editChannelOption = element(by.id(this.testID.editChannelOption)); convertPrivateOption = element(by.id(this.testID.convertPrivateOption)); leaveChannelOption = element(by.id(this.testID.leaveChannelOption)); diff --git a/detox/e2e/support/ui/screen/channel_list.ts b/detox/e2e/support/ui/screen/channel_list.ts index 869950e250..d92ba2201d 100644 --- a/detox/e2e/support/ui/screen/channel_list.ts +++ b/detox/e2e/support/ui/screen/channel_list.ts @@ -17,7 +17,6 @@ class ChannelListScreen { serverIcon: 'channel_list.servers.server_icon', headerTeamDisplayName: 'channel_list_header.team_display_name', headerServerDisplayName: 'channel_list_header.server_display_name', - headerChevronButton: 'channel_list_header.chevron.button', headerPlusButton: 'channel_list_header.plus.button', subheaderSearchFieldButton: 'channel_list_subheader.search_field.button', findChannelsInput: 'channel_list.search_field.find_channels.input', @@ -28,7 +27,6 @@ class ChannelListScreen { serverIcon = element(by.id(this.testID.serverIcon)); headerTeamDisplayName = element(by.id(this.testID.headerTeamDisplayName)); headerServerDisplayName = element(by.id(this.testID.headerServerDisplayName)); - headerChevronButton = element(by.id(this.testID.headerChevronButton)); headerPlusButton = element(by.id(this.testID.headerPlusButton)); subheaderSearchFieldButton = element(by.id(this.testID.subheaderSearchFieldButton)); findChannelsInput = element(by.id(this.testID.findChannelsInput)); diff --git a/detox/e2e/support/ui/screen/create_direct_message.ts b/detox/e2e/support/ui/screen/create_direct_message.ts index b3eb7fc2c2..b97e60ff81 100644 --- a/detox/e2e/support/ui/screen/create_direct_message.ts +++ b/detox/e2e/support/ui/screen/create_direct_message.ts @@ -3,7 +3,7 @@ import {ProfilePicture} from '@support/ui/component'; import {ChannelListScreen} from '@support/ui/screen'; -import {timeouts} from '@support/utils'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; class CreateDirectMessageScreen { @@ -19,6 +19,7 @@ class CreateDirectMessageScreen { flatUserList: 'create_direct_message.user_list.flat_list', sectionUserList: 'create_direct_message.user_list.section_list', tutorialHighlight: 'tutorial_highlight', + tutorialSwipeLeft: 'tutorial_swipe_left', }; createDirectMessageScreen = element(by.id(this.testID.createDirectMessageScreen)); @@ -30,6 +31,7 @@ class CreateDirectMessageScreen { flatUserList = element(by.id(this.testID.flatUserList)); sectionUserList = element(by.id(this.testID.sectionUserList)); tutorialHighlight = element(by.id(this.testID.tutorialHighlight)); + tutorialSwipeLeft = element(by.id(this.testID.tutorialSwipeLeft)); getSelectedUser = (userId: string) => { return element(by.id(`${this.testID.selectedUserPrefix}${userId}`)); @@ -64,6 +66,7 @@ class CreateDirectMessageScreen { open = async () => { // # Open create direct message screen await ChannelListScreen.headerPlusButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelListScreen.openDirectMessageItem.tap(); return this.toBeVisible(); @@ -76,7 +79,7 @@ class CreateDirectMessageScreen { closeTutorial = async () => { await expect(this.tutorialHighlight).toExist(); - await this.closeButton.tap(); + await this.tutorialSwipeLeft.tap(); await expect(this.tutorialHighlight).not.toExist(); }; } diff --git a/detox/e2e/support/ui/screen/create_or_edit_channel.ts b/detox/e2e/support/ui/screen/create_or_edit_channel.ts index 83e2c996e6..70f5dc9de4 100644 --- a/detox/e2e/support/ui/screen/create_or_edit_channel.ts +++ b/detox/e2e/support/ui/screen/create_or_edit_channel.ts @@ -6,7 +6,7 @@ import { ChannelScreen, ChannelListScreen, } from '@support/ui/screen'; -import {timeouts} from '@support/utils'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; class CreateOrEditChannelScreen { @@ -51,6 +51,7 @@ class CreateOrEditChannelScreen { openCreateChannel = async () => { // # Open create channel screen await ChannelListScreen.headerPlusButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelListScreen.createNewChannelItem.tap(); return this.toBeVisible(); diff --git a/detox/e2e/support/ui/screen/login.ts b/detox/e2e/support/ui/screen/login.ts index a87a402b64..c78119eb0f 100644 --- a/detox/e2e/support/ui/screen/login.ts +++ b/detox/e2e/support/ui/screen/login.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {ServerScreen} from '@support/ui/screen'; -import {timeouts} from '@support/utils'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; class LoginScreen { @@ -62,6 +62,7 @@ class LoginScreen { await this.usernameInput.replaceText(user.username); await this.passwordInput.replaceText(user.password); await this.signinButton.tap(); + await wait(timeouts.ONE_SEC); }; } diff --git a/detox/e2e/support/ui/screen/server_list.ts b/detox/e2e/support/ui/screen/server_list.ts index a3db0a096f..13a31b5624 100644 --- a/detox/e2e/support/ui/screen/server_list.ts +++ b/detox/e2e/support/ui/screen/server_list.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {ChannelListScreen} from '@support/ui/screen'; -import {timeouts, wait} from '@support/utils'; +import {timeouts} from '@support/utils'; import {expect} from 'detox'; class ServerListScreen { @@ -11,12 +11,16 @@ class ServerListScreen { serverListBackdrop: 'server_list.backdrop', serverListTitle: 'server_list.title', addServerButton: 'server_list.add_a_server.button', + tutorialHighlight: 'tutorial_highlight', + tutorialSwipeLeft: 'tutorial_swipe_left', }; serverListScreen = element(by.id(this.testID.serverListScreen)); serverListBackdrop = element(by.id(this.testID.serverListBackdrop)); serverListTitle = element(by.id(this.testID.serverListTitle)); addServerButton = element(by.id(this.testID.addServerButton)); + tutorialHighlight = element(by.id(this.testID.tutorialHighlight)); + tutorialSwipeLeft = element(by.id(this.testID.tutorialSwipeLeft)); toServerItemTestIdPrefix = (serverDisplayName: string) => { return `server_list.server_item.${serverDisplayName.replace(/ /g, '_').toLocaleLowerCase()}`; @@ -60,10 +64,6 @@ class ServerListScreen { // # Open server list screen await ChannelListScreen.serverIcon.tap(); - // # Close tip overlay - await wait(timeouts.FOUR_SEC); - await this.serverListScreen.tap({x: 5, y: 10}); - return this.toBeVisible(); }; @@ -71,6 +71,12 @@ class ServerListScreen { await this.serverListBackdrop.tap({x: 5, y: 10}); await expect(this.serverListScreen).not.toBeVisible(); }; + + closeTutorial = async () => { + await expect(this.tutorialHighlight).toExist(); + await this.tutorialSwipeLeft.tap(); + await expect(this.tutorialHighlight).not.toExist(); + }; } const serverListScreen = new ServerListScreen(); diff --git a/detox/e2e/test/autocomplete/create_channel.e2e.ts b/detox/e2e/test/autocomplete/create_channel.e2e.ts index 661c257ddc..1aff217e09 100644 --- a/detox/e2e/test/autocomplete/create_channel.e2e.ts +++ b/detox/e2e/test/autocomplete/create_channel.e2e.ts @@ -20,6 +20,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; +import {timeouts} from '@support/utils'; import {expect} from 'detox'; describe('Autocomplete - Create Channel', () => { @@ -58,7 +59,7 @@ describe('Autocomplete - Create Channel', () => { await CreateOrEditChannelScreen.headerInput.typeText('@'); // * Verify at-mention list is displayed - await expect(Autocomplete.sectionAtMentionList).toBeVisible(); + await waitFor(Autocomplete.sectionAtMentionList).toBeVisible().withTimeout(timeouts.ONE_SEC); }); it('MM-T4904_2 - should render channel mention autocomplete in header input', async () => { diff --git a/detox/e2e/test/autocomplete/edit_channel.e2e.ts b/detox/e2e/test/autocomplete/edit_channel.e2e.ts index fe1f5c5ecb..39903807a1 100644 --- a/detox/e2e/test/autocomplete/edit_channel.e2e.ts +++ b/detox/e2e/test/autocomplete/edit_channel.e2e.ts @@ -22,6 +22,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; +import {timeouts} from '@support/utils'; import {expect} from 'detox'; describe('Autocomplete - Edit Channel', () => { @@ -65,7 +66,7 @@ describe('Autocomplete - Edit Channel', () => { await CreateOrEditChannelScreen.headerInput.typeText('@'); // * Verify at-mention list is displayed - await expect(Autocomplete.sectionAtMentionList).toBeVisible(); + await waitFor(Autocomplete.sectionAtMentionList).toBeVisible().withTimeout(timeouts.ONE_SEC); }); it('MM-T4885_2 - should render channel mention autocomplete in header input', async () => { diff --git a/detox/e2e/test/channels/channel_info.e2e.ts b/detox/e2e/test/channels/channel_info.e2e.ts index e09ce7b77b..87b772e884 100644 --- a/detox/e2e/test/channels/channel_info.e2e.ts +++ b/detox/e2e/test/channels/channel_info.e2e.ts @@ -20,6 +20,7 @@ import { ServerScreen, ChannelInfoScreen, } from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Channel Info', () => { @@ -59,11 +60,11 @@ describe('Channels - Channel Info', () => { await expect(ChannelInfoScreen.favoriteAction).toBeVisible(); await expect(ChannelInfoScreen.muteAction).toBeVisible(); await expect(ChannelInfoScreen.addPeopleAction).toBeVisible(); - await expect(ChannelInfoScreen.copyChannelLinkAction).toBeVisible(); + await expect(ChannelInfoScreen.joinStartCallAction).toBeVisible(); await expect(ChannelInfoScreen.ignoreMentionsOptionToggledOff).toBeVisible(); - await expect(ChannelInfoScreen.notificationPreferenceOption).toBeVisible(); await expect(ChannelInfoScreen.pinnedMessagesOption).toBeVisible(); await expect(ChannelInfoScreen.membersOption).toBeVisible(); + await expect(ChannelInfoScreen.copyChannelLinkOption).toBeVisible(); await expect(ChannelInfoScreen.editChannelOption).toBeVisible(); await expect(ChannelInfoScreen.leaveChannelOption).toBeVisible(); await expect(ChannelInfoScreen.archiveChannelOption).toBeVisible(); @@ -90,6 +91,7 @@ describe('Channels - Channel Info', () => { // # Open a channel screen, tap on channel quick actions button, and tap on channel info action await ChannelScreen.open(channelsCategory, testChannel.name); await ChannelScreen.channelQuickActionsButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelScreen.channelInfoQuickAction.tap(); // * Verify on channel info screen diff --git a/detox/e2e/test/channels/channel_list.e2e.ts b/detox/e2e/test/channels/channel_list.e2e.ts index 6b33c6bb26..63c545fe7b 100644 --- a/detox/e2e/test/channels/channel_list.e2e.ts +++ b/detox/e2e/test/channels/channel_list.e2e.ts @@ -27,6 +27,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Channel List', () => { @@ -65,7 +66,6 @@ describe('Channels - Channel List', () => { await expect(ChannelListScreen.serverIcon).toBeVisible(); await expect(ChannelListScreen.headerTeamDisplayName).toHaveText(testTeam.display_name); await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverOneDisplayName); - await expect(ChannelListScreen.headerChevronButton).toBeVisible(); await expect(ChannelListScreen.headerPlusButton).toBeVisible(); await expect(ChannelListScreen.threadsButton).toBeVisible(); await expect(ChannelListScreen.getCategoryHeaderDisplayName(channelsCategory)).toHaveText('CHANNELS'); @@ -128,6 +128,7 @@ describe('Channels - Channel List', () => { it('MM-T4728_4 - should be able to go to browse channels screen', async () => { // # Tap on plus menu button and tap on browse channels item await ChannelListScreen.headerPlusButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelListScreen.browseChannelsItem.tap(); // * Verify on browse channels screen @@ -140,6 +141,7 @@ describe('Channels - Channel List', () => { it('MM-T4728_5 - should be able to go to create direct message screen', async () => { // # Tap on plus menu button and tap on open a direct message item await ChannelListScreen.headerPlusButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelListScreen.openDirectMessageItem.tap(); // * Verify on create direct message screen @@ -153,6 +155,7 @@ describe('Channels - Channel List', () => { it('MM-T4728_6 - should be able to go to create channel screen', async () => { // # Tap on plus menu button and tap on create new channel item await ChannelListScreen.headerPlusButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelListScreen.createNewChannelItem.tap(); // * Verify on create channel screen @@ -193,7 +196,7 @@ describe('Channels - Channel List', () => { // * Verify on first team and team sidebar item is selected and has correct display name abbreviation await expect(ChannelListScreen.headerTeamDisplayName).toHaveText(testTeam.display_name); await expect(ChannelListScreen.getTeamItemSelected(testTeam.id)).toBeVisible(); - await expect(ChannelListScreen.getTeamItemDisplayNameAbbreviation(testTeam.id)).toHaveText(testTeam.display_name.substring(0, 2)); + await expect(ChannelListScreen.getTeamItemDisplayNameAbbreviation(testTeam.id)).toHaveText(testTeam.display_name.substring(0, 2).toUpperCase()); // # Tap on second team item from team sidebar await ChannelListScreen.getTeamItemNotSelected(testTeamTwo.id).tap(); @@ -201,7 +204,7 @@ describe('Channels - Channel List', () => { // * Verify on second team and team sidebar item is selected and has correct display name abbreviation await expect(ChannelListScreen.headerTeamDisplayName).toHaveText(testTeamTwo.display_name); await expect(ChannelListScreen.getTeamItemSelected(testTeamTwo.id)).toBeVisible(); - await expect(ChannelListScreen.getTeamItemDisplayNameAbbreviation(testTeamTwo.id)).toHaveText(testTeamTwo.display_name.substring(0, 2)); + await expect(ChannelListScreen.getTeamItemDisplayNameAbbreviation(testTeamTwo.id)).toHaveText(testTeamTwo.display_name.substring(0, 2).toUpperCase()); // # Tap back on first team item from team sidebar await ChannelListScreen.getTeamItemNotSelected(testTeam.id).tap(); diff --git a/detox/e2e/test/channels/create_direct_message.e2e.ts b/detox/e2e/test/channels/create_direct_message.e2e.ts index e11db9275b..796f03acd9 100644 --- a/detox/e2e/test/channels/create_direct_message.e2e.ts +++ b/detox/e2e/test/channels/create_direct_message.e2e.ts @@ -60,7 +60,6 @@ describe('Channels - Create Direct Message', () => { // * Verify basic elements on create direct message screen await expect(CreateDirectMessageScreen.closeButton).toBeVisible(); - await expect(CreateDirectMessageScreen.startButton).toBeVisible(); await expect(CreateDirectMessageScreen.searchInput).toBeVisible(); await expect(CreateDirectMessageScreen.sectionUserList).toBeVisible(); diff --git a/detox/e2e/test/channels/favorite_and_unfavorite_channel.e2e.ts b/detox/e2e/test/channels/favorite_and_unfavorite_channel.e2e.ts index a741da37bc..ef85d10151 100644 --- a/detox/e2e/test/channels/favorite_and_unfavorite_channel.e2e.ts +++ b/detox/e2e/test/channels/favorite_and_unfavorite_channel.e2e.ts @@ -9,7 +9,6 @@ import { Channel, - Post, Setup, Team, User, @@ -21,11 +20,13 @@ import { import { ChannelScreen, ChannelListScreen, + CreateDirectMessageScreen, HomeScreen, LoginScreen, ServerScreen, ChannelInfoScreen, } from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Favorite and Unfavorite Channel', () => { @@ -62,9 +63,11 @@ describe('Channels - Favorite and Unfavorite Channel', () => { // # Open a channel screen, tap on channel quick actions button, and tap on favorite quick action to favorite the channel await ChannelScreen.open(channelsCategory, testChannel.name); await ChannelScreen.channelQuickActionsButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelScreen.favoriteQuickAction.tap(); // * Verify favorited toast message appears + await wait(timeouts.ONE_SEC); await expect(ChannelScreen.toastMessage).toHaveText('This channel was favorited'); // # Go back to channel list screen @@ -76,9 +79,11 @@ describe('Channels - Favorite and Unfavorite Channel', () => { // # Go back to the favorited channel, tap on channel quick actions button, and tap on favorited quick action to unfavorite the channel await ChannelScreen.open(favoritesCategory, testChannel.name); await ChannelScreen.channelQuickActionsButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelScreen.unfavoriteQuickAction.tap(); // * Verify unfavorited toast message appears + await wait(timeouts.ONE_SEC); await expect(ChannelScreen.toastMessage).toHaveText('This channel was unfavorited'); // # Go back to channel list screen @@ -117,17 +122,19 @@ describe('Channels - Favorite and Unfavorite Channel', () => { const {user: newUser} = await User.apiCreateUser(siteOneUrl); await Team.apiAddUserToTeam(siteOneUrl, newUser.id, testTeam.id); const {channel: directMessageChannel} = await Channel.apiCreateDirectChannel(siteOneUrl, [testUser.id, newUser.id]); - await Post.apiCreatePost(siteOneUrl, { - channelId: directMessageChannel.id, - message: 'test', - }); + await CreateDirectMessageScreen.open(); + await CreateDirectMessageScreen.closeTutorial(); + await CreateDirectMessageScreen.searchInput.replaceText(newUser.username); + await CreateDirectMessageScreen.getUserItem(newUser.id).tap(); + await CreateDirectMessageScreen.startButton.tap(); + await ChannelScreen.postMessage('test'); await device.reloadReactNative(); - await ChannelScreen.open(directMessagesCategory, directMessageChannel.name); + await ChannelListScreen.getChannelItemDisplayName(directMessagesCategory, directMessageChannel.name).tap(); await ChannelScreen.introFavoriteAction.tap(); await ChannelScreen.back(); // * Verify direct message channel is listed under favorites category - await expect(ChannelListScreen.getChannelItemDisplayName(favoritesCategory, directMessageChannel.name)).toBeVisible(); + await expect(ChannelListScreen.getChannelItemDisplayName(favoritesCategory, directMessageChannel.name)).toHaveText(newUser.username); // # Go back to the favorited direct message channel, tap on intro favorited action to unfavorite the direct message channel, and go back to channel list screen await ChannelScreen.open(favoritesCategory, directMessageChannel.name); diff --git a/detox/e2e/test/channels/leave_channel.e2e.ts b/detox/e2e/test/channels/leave_channel.e2e.ts index 741fc19505..d3d9d007c4 100644 --- a/detox/e2e/test/channels/leave_channel.e2e.ts +++ b/detox/e2e/test/channels/leave_channel.e2e.ts @@ -23,6 +23,7 @@ import { ServerScreen, ChannelInfoScreen, } from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Leave Channel', () => { @@ -89,6 +90,7 @@ describe('Channels - Leave Channel', () => { await device.reloadReactNative(); await ChannelScreen.open(channelsCategory, channel.name); await ChannelScreen.channelQuickActionsButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelScreen.leaveChannel({confirm: true}); // * Verify on channel list screen and the channel left by the user does not appear on the list diff --git a/detox/e2e/test/channels/mute_and_unmute_channel.e2e.ts b/detox/e2e/test/channels/mute_and_unmute_channel.e2e.ts index 95e5e4853c..be68a11624 100644 --- a/detox/e2e/test/channels/mute_and_unmute_channel.e2e.ts +++ b/detox/e2e/test/channels/mute_and_unmute_channel.e2e.ts @@ -20,6 +20,7 @@ import { ServerScreen, ChannelInfoScreen, } from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Mute and Unmute Channel', () => { @@ -50,16 +51,20 @@ describe('Channels - Mute and Unmute Channel', () => { // # Open a channel screen, tap on channel quick actions button, and tap on mute quick action to mute the channel await ChannelScreen.open(channelsCategory, testChannel.name); await ChannelScreen.channelQuickActionsButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelScreen.muteQuickAction.tap(); // * Verify muted toast message appears + await wait(timeouts.ONE_SEC); await expect(ChannelScreen.toastMessage).toHaveText('This channel was muted'); // # Tap on channel quick actions button and tap on muted quick action to unmute the channel await ChannelScreen.channelQuickActionsButton.tap(); + await wait(timeouts.ONE_SEC); await ChannelScreen.unmuteQuickAction.tap(); // * Verify unmuted toast message appears + await wait(timeouts.ONE_SEC); await expect(ChannelScreen.toastMessage).toHaveText('This channel was unmuted'); // # Go back to channel list screen diff --git a/detox/e2e/test/messaging/save_and_unsave_message.e2e.ts b/detox/e2e/test/messaging/save_and_unsave_message.e2e.ts index edaaa14825..eeb5d9e1d5 100644 --- a/detox/e2e/test/messaging/save_and_unsave_message.e2e.ts +++ b/detox/e2e/test/messaging/save_and_unsave_message.e2e.ts @@ -24,7 +24,7 @@ import { ServerScreen, ThreadScreen, } from '@support/ui/screen'; -import {getRandomId} from '@support/utils'; +import {getRandomId, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Messaging - Save and Unsave Message', () => { @@ -102,6 +102,7 @@ describe('Messaging - Save and Unsave Message', () => { await PostOptionsScreen.unsavePostOption.tap(); // * Verify saved text is not displayed on the post pre-header + await wait(timeouts.ONE_SEC); await expect(postListPostItemPreHeaderText).not.toBeVisible(); // # Go back to channel list screen diff --git a/detox/e2e/test/server_login/connect_to_server.e2e.ts b/detox/e2e/test/server_login/connect_to_server.e2e.ts index b7b7970eaa..bef50ccd97 100644 --- a/detox/e2e/test/server_login/connect_to_server.e2e.ts +++ b/detox/e2e/test/server_login/connect_to_server.e2e.ts @@ -12,6 +12,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Server Login - Connect to Server', () => { @@ -91,6 +92,7 @@ describe('Server Login - Connect to Server', () => { await connectButton.tap(); // * Verify connection error + await wait(timeouts.ONE_SEC); await expect(serverUrlInputError).toHaveText(connectionError); }); diff --git a/detox/e2e/test/server_login/server_list.e2e.ts b/detox/e2e/test/server_login/server_list.e2e.ts index d5599bbec1..b4d21e5e04 100644 --- a/detox/e2e/test/server_login/server_list.e2e.ts +++ b/detox/e2e/test/server_login/server_list.e2e.ts @@ -30,6 +30,7 @@ import { ServerScreen, ServerListScreen, } from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Server Login - Server List', () => { @@ -61,6 +62,7 @@ describe('Server Login - Server List', () => { it('MM-T4691_1 - should match elements on server list screen', async () => { // # Open server list screen await ServerListScreen.open(); + await ServerListScreen.closeTutorial(); // * Verify basic elements on server list screen await expect(ServerListScreen.serverListTitle).toHaveText('Your servers'); @@ -91,7 +93,6 @@ describe('Server Login - Server List', () => { // * Verify on channel list screen of the second server await ChannelListScreen.toBeVisible(); - await device.reloadReactNative(); await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverTwoDisplayName); // # Open server list screen @@ -195,6 +196,7 @@ describe('Server Login - Server List', () => { // # Tap on remove button and go back to server list screen await Alert.removeButton.tap(); + await wait(timeouts.ONE_SEC); await ServerListScreen.open(); // * Verify first server is removed diff --git a/detox/e2e/test/smoke_test/messaging.e2e.ts b/detox/e2e/test/smoke_test/messaging.e2e.ts index fd92182a41..64065d4905 100644 --- a/detox/e2e/test/smoke_test/messaging.e2e.ts +++ b/detox/e2e/test/smoke_test/messaging.e2e.ts @@ -27,7 +27,7 @@ import { ServerScreen, ThreadScreen, } from '@support/ui/screen'; -import {getRandomId} from '@support/utils'; +import {getRandomId, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Smoke Test - Messaging', () => { @@ -189,6 +189,7 @@ describe('Smoke Test - Messaging', () => { await PostOptionsScreen.pinPostOption.tap(); // * Verify pinned text is displayed on the post pre-header + await wait(timeouts.ONE_SEC); const {postListPostItemPreHeaderText: threadPostListPostItemPreHeaderText} = ThreadScreen.getPostListPostItem(post.id, message); await expect(threadPostListPostItemPreHeaderText).toHaveText(pinnedText); diff --git a/detox/e2e/test/smoke_test/server_login.e2e.ts b/detox/e2e/test/smoke_test/server_login.e2e.ts index c01d31b386..6178112e8f 100644 --- a/detox/e2e/test/smoke_test/server_login.e2e.ts +++ b/detox/e2e/test/smoke_test/server_login.e2e.ts @@ -59,6 +59,7 @@ describe('Smoke Test - Server Login', () => { it('MM-T4675_2 - should be able to add a new server and log-in-to/log-out-from the new server', async () => { // # Open server list screen await ServerListScreen.open(); + await ServerListScreen.closeTutorial(); // * Verify on server list screen await ServerListScreen.toBeVisible(); @@ -73,7 +74,6 @@ describe('Smoke Test - Server Login', () => { // * Verify on channel list screen of the second server await ChannelListScreen.toBeVisible(); - await device.reloadReactNative(); await expect(ChannelListScreen.headerServerDisplayName).toHaveText(serverTwoDisplayName); // # Go back to first server, open server list screen, swipe left on second server and tap on logout option diff --git a/detox/e2e/test/smoke_test/threads.e2e.ts b/detox/e2e/test/smoke_test/threads.e2e.ts index c5ecfa63e1..864446a9f6 100644 --- a/detox/e2e/test/smoke_test/threads.e2e.ts +++ b/detox/e2e/test/smoke_test/threads.e2e.ts @@ -26,7 +26,7 @@ import { ThreadOptionsScreen, ThreadScreen, } from '@support/ui/screen'; -import {getRandomId} from '@support/utils'; +import {getRandomId, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Smoke Test - Threads', () => { @@ -91,6 +91,7 @@ describe('Smoke Test - Threads', () => { await ThreadOptionsScreen.markAsReadOption.tap(); // * Verify thread is not displayed anymore in unread threads section + await wait(timeouts.ONE_SEC); await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).not.toBeVisible(); // # Tap on all your threads button, tap on the thread, and add new reply to thread @@ -133,6 +134,7 @@ describe('Smoke Test - Threads', () => { await ThreadScreen.back(); await GlobalThreadsScreen.openThreadOptionsFor(parentPost.id); await ThreadOptionsScreen.unsaveThreadOption.tap(); + await wait(timeouts.ONE_SEC); await GlobalThreadsScreen.getThreadItem(parentPost.id).tap(); // * Verify saved text is not displayed on the post pre-header diff --git a/detox/e2e/test/teams/invite_people.e2e.ts b/detox/e2e/test/teams/invite_people.e2e.ts index 3423006fdc..f325701d7e 100644 --- a/detox/e2e/test/teams/invite_people.e2e.ts +++ b/detox/e2e/test/teams/invite_people.e2e.ts @@ -18,7 +18,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; -import {isIos} from '@support/utils'; +import {isIos, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; function systemDialog(label: string) { @@ -51,9 +51,6 @@ describe('Teams - Invite people', () => { }); afterAll(async () => { - // # Close share dialog - await ChannelListScreen.headerTeamDisplayName.tap(); - // # Log out await HomeScreen.logout(); }); @@ -63,6 +60,7 @@ describe('Teams - Invite people', () => { await ChannelListScreen.headerPlusButton.tap(); // * Verify invite people to team item is available + await wait(timeouts.ONE_SEC); await expect(ChannelListScreen.invitePeopleToTeamItem).toExist(); // # Tap on invite people to team item @@ -71,6 +69,9 @@ describe('Teams - Invite people', () => { if (isIos()) { // * Verify share dialog is open await expect(systemDialog(`Join the ${testTeam.display_name} team`)).toExist(); + + // # Close share dialog + await device.reloadReactNative(); } }); }); diff --git a/detox/e2e/test/threads/mark_thread_as_read_and_unread.e2e.ts b/detox/e2e/test/threads/mark_thread_as_read_and_unread.e2e.ts index e58b232ff3..a9f2120403 100644 --- a/detox/e2e/test/threads/mark_thread_as_read_and_unread.e2e.ts +++ b/detox/e2e/test/threads/mark_thread_as_read_and_unread.e2e.ts @@ -26,7 +26,7 @@ import { ThreadOptionsScreen, ThreadScreen, } from '@support/ui/screen'; -import {getRandomId} from '@support/utils'; +import {getRandomId, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Threads - Mark Thread as Read and Unread', () => { @@ -119,6 +119,7 @@ describe('Threads - Mark Thread as Read and Unread', () => { await ThreadOptionsScreen.markAsReadOption.tap(); // * Verify thread is not displayed anymore in unread threads section + await wait(timeouts.ONE_SEC); await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).not.toBeVisible(); // # Tap on all your threads button From e294b07418dced6b79dd5eb47743a76c3d7e8709 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Fri, 16 Dec 2022 18:57:15 +0200 Subject: [PATCH 02/71] Add DeepLink support (#6869) --- app/actions/remote/channel.ts | 5 +- app/actions/remote/command.ts | 69 +------ app/actions/remote/permalink.ts | 7 +- .../markdown/markdown_link/markdown_link.tsx | 46 ++--- app/components/post_list/post/post.tsx | 2 +- app/constants/deep_linking.ts | 1 + app/constants/launch.ts | 1 + app/init/launch.ts | 76 ++------ app/managers/global_event_handler.ts | 5 +- app/managers/session_manager.ts | 4 +- .../components/options/logout/index.tsx | 3 + .../servers_list/server_item/server_item.tsx | 6 +- app/screens/home/index.tsx | 24 ++- .../option_menus/option_menus.tsx | 2 +- app/screens/login/index.tsx | 1 + app/screens/navigation.ts | 5 +- app/screens/onboarding/index.tsx | 3 +- app/screens/permalink/permalink.tsx | 3 +- app/screens/server/index.tsx | 9 +- .../options/open_in_channel_option.tsx | 2 +- app/utils/deep_link/index.ts | 183 ++++++++++++++++++ app/utils/server/index.ts | 6 +- app/utils/url/index.ts | 65 +------ app/utils/url/test.ts | 18 +- types/launch/index.ts | 1 + 25 files changed, 295 insertions(+), 252 deletions(-) create mode 100644 app/utils/deep_link/index.ts diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 8d8085da07..780ebf190d 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -10,7 +10,7 @@ import {addChannelToDefaultCategory, storeCategories} from '@actions/local/categ import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel'; import {switchToGlobalThreads} from '@actions/local/thread'; import {loadCallForChannel} from '@calls/actions/calls'; -import {Events, General, Preferences, Screens} from '@constants'; +import {DeepLink, Events, General, Preferences, Screens} from '@constants'; import DatabaseManager from '@database/manager'; import {privateChannelJoinPrompt} from '@helpers/api/channel'; import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; @@ -28,7 +28,6 @@ import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from import {isTablet} from '@utils/helpers'; import {logDebug, logError, logInfo} from '@utils/log'; import {showMuteChannelSnackbar} from '@utils/snack_bar'; -import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url'; import {displayGroupMessageName, displayUsername} from '@utils/user'; import {fetchGroupsForChannelIfConstrained} from './groups'; @@ -655,7 +654,7 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri let joinedTeam = false; let teamId = ''; try { - if (teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) { + if (teamName === DeepLink.Redirect) { teamId = await getCurrentTeamId(database); } else { const team = await getTeamByName(database, teamName); diff --git a/app/actions/remote/command.ts b/app/actions/remote/command.ts index 4cb475925f..6ecd317fe3 100644 --- a/app/actions/remote/command.ts +++ b/app/actions/remote/command.ts @@ -5,26 +5,18 @@ import {IntlShape} from 'react-intl'; import {Alert} from 'react-native'; import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/remote/apps'; -import {showPermalink} from '@actions/remote/permalink'; import {Client} from '@client/rest'; import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser'; import {AppCallResponseTypes} from '@constants/apps'; -import DeepLinkType from '@constants/deep_linking'; import DatabaseManager from '@database/manager'; import AppsManager from '@managers/apps_manager'; import IntegrationsManager from '@managers/integrations_manager'; import NetworkManager from '@managers/network_manager'; import {getChannelById} from '@queries/servers/channel'; import {getConfig, getCurrentTeamId} from '@queries/servers/system'; -import {getTeammateNameDisplay, queryUsersByUsername} from '@queries/servers/user'; -import {showAppForm, showModal} from '@screens/navigation'; -import * as DraftUtils from '@utils/draft'; -import {matchDeepLink, tryOpenURL} from '@utils/url'; -import {displayUsername} from '@utils/user'; - -import {makeDirectChannel, switchToChannelById, switchToChannelByName} from './channel'; - -import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkDM, DeepLinkGM, DeepLinkPlugin} from '@typings/launch'; +import {showAppForm} from '@screens/navigation'; +import {handleDeepLink, matchDeepLink} from '@utils/deep_link'; +import {tryOpenURL} from '@utils/url'; export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: string | {message: string}}> => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; @@ -144,60 +136,9 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc const config = await getConfig(database); const match = matchDeepLink(location, serverUrl, config?.SiteURL); - let linkServerUrl: string | undefined; - if (match?.data?.serverUrl) { - linkServerUrl = DatabaseManager.searchUrl(match.data.serverUrl); - } - if (match && linkServerUrl) { - switch (match.type) { - case DeepLinkType.Channel: { - const data = match.data as DeepLinkChannel; - switchToChannelByName(linkServerUrl, data.channelName, data.teamName, DraftUtils.errorBadChannel, intl); - break; - } - case DeepLinkType.Permalink: { - const data = match.data as DeepLinkPermalink; - showPermalink(linkServerUrl, data.teamName, data.postId, intl); - break; - } - case DeepLinkType.DirectMessage: { - const data = match.data as DeepLinkDM; - if (!data.userName) { - DraftUtils.errorUnkownUser(intl); - return {data: false}; - } - - if (data.serverUrl !== serverUrl) { - if (!database) { - return {error: `${serverUrl} database not found`}; - } - } - const user = (await queryUsersByUsername(database, [data.userName]).fetch())[0]; - if (!user) { - DraftUtils.errorUnkownUser(intl); - return {data: false}; - } - - makeDirectChannel(linkServerUrl, user.id, displayUsername(user, intl.locale, await getTeammateNameDisplay(database)), true); - break; - } - case DeepLinkType.GroupMessage: { - const data = match.data as DeepLinkGM; - if (!data.channelId) { - DraftUtils.errorBadChannel(intl); - return {data: false}; - } - - switchToChannelById(linkServerUrl, data.channelId); - break; - } - case DeepLinkType.Plugin: { - const data = match.data as DeepLinkPlugin; - showModal('PluginInternal', data.id, {link: location}); - break; - } - } + if (match) { + handleDeepLink(location, intl, location); } else { const {formatMessage} = intl; const onError = () => Alert.alert( diff --git a/app/actions/remote/permalink.ts b/app/actions/remote/permalink.ts index 19aa9b3b42..19ef622ccb 100644 --- a/app/actions/remote/permalink.ts +++ b/app/actions/remote/permalink.ts @@ -1,15 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {DeepLink} from '@constants'; import DatabaseManager from '@database/manager'; import {getCurrentTeam} from '@queries/servers/team'; import {displayPermalink} from '@utils/permalink'; -import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url'; import type TeamModel from '@typings/database/models/servers/team'; -import type {IntlShape} from 'react-intl'; -export const showPermalink = async (serverUrl: string, teamName: string, postId: string, intl: IntlShape, openAsPermalink = true) => { +export const showPermalink = async (serverUrl: string, teamName: string, postId: string, openAsPermalink = true) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; @@ -18,7 +17,7 @@ export const showPermalink = async (serverUrl: string, teamName: string, postId: try { let name = teamName; let team: TeamModel | undefined; - if (!name || name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) { + if (!name || name === DeepLink.Redirect) { team = await getCurrentTeam(database); if (team) { name = team.name; diff --git a/app/components/markdown/markdown_link/markdown_link.tsx b/app/components/markdown/markdown_link/markdown_link.tsx index 1189b4a92f..9bab60b803 100644 --- a/app/components/markdown/markdown_link/markdown_link.tsx +++ b/app/components/markdown/markdown_link/markdown_link.tsx @@ -9,19 +9,14 @@ import {Alert, StyleSheet, Text, View} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import urlParse from 'url-parse'; -import {switchToChannelByName} from '@actions/remote/channel'; -import {showPermalink} from '@actions/remote/permalink'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; -import DeepLinkType from '@constants/deep_linking'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {bottomSheet, dismissBottomSheet} from '@screens/navigation'; -import {errorBadChannel} from '@utils/draft'; +import {handleDeepLink, matchDeepLink} from '@utils/deep_link'; import {bottomSheetSnapPoint} from '@utils/helpers'; import {preventDoubleTap} from '@utils/tap'; -import {matchDeepLink, normalizeProtocol, tryOpenURL} from '@utils/url'; - -import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkWithData} from '@typings/launch'; +import {normalizeProtocol, tryOpenURL} from '@utils/url'; type MarkdownLinkProps = { children: ReactElement; @@ -65,28 +60,27 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU return; } - const match: DeepLinkWithData | null = matchDeepLink(url, serverUrl, siteURL); + const onError = () => { + Alert.alert( + formatMessage({ + id: 'mobile.link.error.title', + defaultMessage: 'Error', + }), + formatMessage({ + id: 'mobile.link.error.text', + defaultMessage: 'Unable to open the link.', + }), + ); + }; - if (match && match.data?.teamName) { - if (match.type === DeepLinkType.Channel) { - await switchToChannelByName(serverUrl, (match?.data as DeepLinkChannel).channelName, match.data?.teamName, errorBadChannel, intl); - } else if (match.type === DeepLinkType.Permalink) { - showPermalink(serverUrl, match.data.teamName, (match.data as DeepLinkPermalink).postId, intl); + const match = matchDeepLink(url, serverUrl, siteURL); + + if (match) { + const {error} = await handleDeepLink(url, intl); + if (error) { + tryOpenURL(url, onError); } } else { - const onError = () => { - Alert.alert( - formatMessage({ - id: 'mobile.link.error.title', - defaultMessage: 'Error', - }), - formatMessage({ - id: 'mobile.link.error.text', - defaultMessage: 'Unable to open the link.', - }), - ); - }; - tryOpenURL(url, onError); } }), [href, intl.locale, serverUrl, siteURL]); diff --git a/app/components/post_list/post/post.tsx b/app/components/post_list/post/post.tsx index a3a812483f..a1e6f1107a 100644 --- a/app/components/post_list/post/post.tsx +++ b/app/components/post_list/post/post.tsx @@ -139,7 +139,7 @@ const Post = ({ const handlePostPress = () => { if ([Screens.SAVED_MESSAGES, Screens.MENTIONS, Screens.SEARCH, Screens.PINNED_MESSAGES].includes(location)) { - showPermalink(serverUrl, '', post.id, intl); + showPermalink(serverUrl, '', post.id); return; } diff --git a/app/constants/deep_linking.ts b/app/constants/deep_linking.ts index 61ffa56cb4..f2090831c4 100644 --- a/app/constants/deep_linking.ts +++ b/app/constants/deep_linking.ts @@ -8,6 +8,7 @@ const DeepLinkType = { Invalid: 'invalid', Permalink: 'permalink', Plugin: 'plugin', + Redirect: '_redirect', } as const; export default DeepLinkType; diff --git a/app/constants/launch.ts b/app/constants/launch.ts index d31bc52bcf..696785c528 100644 --- a/app/constants/launch.ts +++ b/app/constants/launch.ts @@ -3,6 +3,7 @@ const LaunchType = { AddServer: 'add-server', + AddServerFromDeepLink: 'add-server-deeplink', Normal: 'normal', DeepLink: 'deeplink', Notification: 'notification', diff --git a/app/init/launch.ts b/app/init/launch.ts index 885705374c..fe8484b990 100644 --- a/app/init/launch.ts +++ b/app/init/launch.ts @@ -7,20 +7,20 @@ import {Notifications} from 'react-native-notifications'; import {appEntry, pushNotificationEntry, upgradeEntry} from '@actions/remote/entry'; import LocalConfig from '@assets/config.json'; -import {Screens, DeepLink, Events, Launch, PushNotification} from '@constants'; +import {DeepLink, Events, Launch, PushNotification} from '@constants'; import DatabaseManager from '@database/manager'; import {getActiveServerUrl, getServerCredentials, removeServerCredentials} from '@init/credentials'; import {getOnboardingViewed} from '@queries/app/global'; import {getThemeForCurrentTeam} from '@queries/servers/preference'; import {getCurrentUserId} from '@queries/servers/system'; import {queryMyTeams} from '@queries/servers/team'; -import {goToScreen, resetToHome, resetToSelectServer, resetToTeams, resetToOnboarding} from '@screens/navigation'; +import {resetToHome, resetToSelectServer, resetToTeams, resetToOnboarding} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; +import {getLaunchPropsFromDeepLink} from '@utils/deep_link'; import {logInfo} from '@utils/log'; import {convertToNotificationData} from '@utils/notification'; -import {parseDeepLink} from '@utils/url'; -import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch'; +import type {DeepLinkWithData, LaunchProps} from '@typings/launch'; const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, PushNotification.NOTIFICATION_TYPE.SESSION]; @@ -67,13 +67,18 @@ const launchAppFromNotification = async (notification: NotificationWithData, col * @returns a redirection to a screen, either onboarding, add_server, login or home depending on the scenario */ -const launchApp = async (props: LaunchProps, resetNavigation = true) => { +const launchApp = async (props: LaunchProps) => { let serverUrl: string | undefined; switch (props?.launchType) { case Launch.DeepLink: if (props.extra?.type !== DeepLink.Invalid) { const extra = props.extra as DeepLinkWithData; - serverUrl = extra.data?.serverUrl; + const existingServer = DatabaseManager.searchUrl(extra.data!.serverUrl); + serverUrl = existingServer; + props.serverUrl = serverUrl || extra.data?.serverUrl; + if (!serverUrl) { + props.launchError = true; + } } break; case Launch.Notification: { @@ -142,17 +147,17 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => { return resetToOnboarding(props); } - return launchToServer(props, resetNavigation); + return resetToSelectServer(props); }; const launchToHome = async (props: LaunchProps) => { let openPushNotification = false; switch (props.launchType) { - case Launch.DeepLink: - // TODO: - // deepLinkEntry({props.serverUrl, props.extra}); + case Launch.DeepLink: { + appEntry(props.serverUrl!); break; + } case Launch.Notification: { const extra = props.extra as NotificationWithData; openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local); @@ -185,55 +190,8 @@ const launchToHome = async (props: LaunchProps) => { return resetToTeams(); }; -const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => { - if (resetNavigation) { - return resetToSelectServer(props); - } - - // This is being called for Deeplinks, but needs to be revisited when - // the implementation of deep links is complete - const title = ''; - return goToScreen(Screens.SERVER, title, {...props}); -}; - -export const relaunchApp = (props: LaunchProps, resetNavigation = false) => { - return launchApp(props, resetNavigation); -}; - -export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = false): LaunchProps => { - const parsed = parseDeepLink(deepLinkUrl); - const launchProps: LaunchProps = { - launchType: Launch.DeepLink, - coldStart, - }; - - switch (parsed.type) { - case DeepLink.Invalid: - launchProps.launchError = true; - break; - case DeepLink.Channel: { - const parsedData = parsed.data as DeepLinkChannel; - (launchProps.extra as DeepLinkWithData).data = parsedData; - break; - } - case DeepLink.DirectMessage: { - const parsedData = parsed.data as DeepLinkDM; - (launchProps.extra as DeepLinkWithData).data = parsedData; - break; - } - case DeepLink.GroupMessage: { - const parsedData = parsed.data as DeepLinkGM; - (launchProps.extra as DeepLinkWithData).data = parsedData; - break; - } - case DeepLink.Permalink: { - const parsedData = parsed.data as DeepLinkPermalink; - (launchProps.extra as DeepLinkWithData).data = parsedData; - break; - } - } - - return launchProps; +export const relaunchApp = (props: LaunchProps) => { + return launchApp(props); }; export const getLaunchPropsFromNotification = async (notification: NotificationWithData, coldStart = false): Promise => { diff --git a/app/managers/global_event_handler.ts b/app/managers/global_event_handler.ts index 7c0f75af4f..30f1f78b2a 100644 --- a/app/managers/global_event_handler.ts +++ b/app/managers/global_event_handler.ts @@ -11,9 +11,9 @@ import {Events, Sso} from '@constants'; import {MIN_REQUIRED_VERSION} from '@constants/supported_server'; import {DEFAULT_LOCALE, getTranslations, t} from '@i18n'; import {getServerCredentials} from '@init/credentials'; -import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch'; import * as analytics from '@managers/analytics'; import {getAllServers} from '@queries/app/servers'; +import {handleDeepLink} from '@utils/deep_link'; import {logError} from '@utils/log'; import type {jsAndNativeErrorHandler} from '@typings/global/error_handling'; @@ -64,8 +64,7 @@ class GlobalEventHandler { } if (event.url) { - const props = getLaunchPropsFromDeepLink(event.url); - relaunchApp(props); + handleDeepLink(event.url); } }; diff --git a/app/managers/session_manager.ts b/app/managers/session_manager.ts index 542be18001..2b3627173c 100644 --- a/app/managers/session_manager.ts +++ b/app/managers/session_manager.ts @@ -185,7 +185,7 @@ class SessionManager { await storeOnboardingViewedValue(false); } - relaunchApp({launchType, serverUrl, displayName}, true); + relaunchApp({launchType, serverUrl, displayName}); } }; @@ -197,7 +197,7 @@ class SessionManager { const activeServerUrl = await DatabaseManager.getActiveServerUrl(); const serverDisplayName = await getServerDisplayName(serverUrl); - await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName}, true); + await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName}); if (activeServerUrl) { addNewServer(getThemeFromState(), serverUrl, serverDisplayName); } else { diff --git a/app/screens/home/account/components/options/logout/index.tsx b/app/screens/home/account/components/options/logout/index.tsx index 0f998a904e..742e3d5114 100644 --- a/app/screens/home/account/components/options/logout/index.tsx +++ b/app/screens/home/account/components/options/logout/index.tsx @@ -3,9 +3,11 @@ import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; +import {Navigation} from 'react-native-navigation'; import {logout} from '@actions/remote/session'; import OptionItem from '@components/option_item'; +import {Screens} from '@constants'; import {useServerDisplayName, useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {alertServerLogout} from '@utils/server'; @@ -30,6 +32,7 @@ const LogOut = () => { const serverDisplayName = useServerDisplayName(); const onLogout = useCallback(preventDoubleTap(() => { + Navigation.updateProps(Screens.HOME, {extra: undefined}); alertServerLogout(serverDisplayName, () => logout(serverUrl), intl); }), [serverDisplayName, serverUrl, intl]); diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx index 81856c084a..8c75f4e30a 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx @@ -6,6 +6,7 @@ import {useIntl} from 'react-intl'; import {Animated, DeviceEventEmitter, Platform, StyleProp, Text, View, ViewStyle} from 'react-native'; import {RectButton} from 'react-native-gesture-handler'; import Swipeable from 'react-native-gesture-handler/Swipeable'; +import {Navigation} from 'react-native-navigation'; import {storeMultiServerTutorial} from '@actions/app/global'; import {appEntry} from '@actions/remote/entry'; @@ -17,7 +18,7 @@ import Loading from '@components/loading'; import ServerIcon from '@components/server_icon'; import TutorialHighlight from '@components/tutorial_highlight'; import TutorialSwipeLeft from '@components/tutorial_highlight/swipe_left'; -import {Events} from '@constants'; +import {Events, Screens} from '@constants'; import {PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; import {useTheme} from '@context/theme'; import DatabaseManager from '@database/manager'; @@ -178,6 +179,7 @@ const ServerItem = ({ }; const logoutServer = async () => { + Navigation.updateProps(Screens.HOME, {extra: undefined}); await logout(server.url); if (isActive) { @@ -190,6 +192,7 @@ const ServerItem = ({ const removeServer = async () => { const skipLogoutFromServer = server.lastActiveAt === 0; await dismissBottomSheet(); + Navigation.updateProps(Screens.HOME, {extra: undefined}); await logout(server.url, skipLogoutFromServer, true); }; @@ -286,6 +289,7 @@ const ServerItem = ({ if (server.lastActiveAt) { setSwitching(true); await dismissBottomSheet(); + Navigation.updateProps(Screens.HOME, {extra: undefined}); DatabaseManager.setActiveServerDatabase(server.url); await appEntry(server.url, Date.now()); return; diff --git a/app/screens/home/index.tsx b/app/screens/home/index.tsx index ca08e7e66a..78598d743b 100644 --- a/app/screens/home/index.tsx +++ b/app/screens/home/index.tsx @@ -14,6 +14,7 @@ import {Events, Screens} from '@constants'; import {useTheme} from '@context/theme'; import {findChannels, popToRoot} from '@screens/navigation'; import NavigationStore from '@store/navigation_store'; +import {handleDeepLink} from '@utils/deep_link'; import {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation'; import {notificationError} from '@utils/notification'; @@ -24,7 +25,7 @@ import SavedMessages from './saved_messages'; import Search from './search'; import TabBar from './tab_bar'; -import type {LaunchProps} from '@typings/launch'; +import type {DeepLinkWithData, LaunchProps} from '@typings/launch'; if (Platform.OS === 'ios') { // We do this on iOS to avoid conflicts betwen ReactNavigation & Wix ReactNativeNavigation @@ -95,6 +96,15 @@ export default function HomeScreen(props: HomeProps) { }; }, [intl.locale]); + useEffect(() => { + if (props.launchType === 'deeplink') { + const deepLink = props.extra as DeepLinkWithData; + if (deepLink?.url) { + handleDeepLink(deepLink.url); + } + } + }, []); + return ( <> ( {() => } diff --git a/app/screens/home/search/results/file_options/option_menus/option_menus.tsx b/app/screens/home/search/results/file_options/option_menus/option_menus.tsx index 2c254273c3..64df7a38fc 100644 --- a/app/screens/home/search/results/file_options/option_menus/option_menus.tsx +++ b/app/screens/home/search/results/file_options/option_menus/option_menus.tsx @@ -42,7 +42,7 @@ const OptionMenus = ({ const handlePermalink = useCallback(() => { if (fileInfo.post_id) { - showPermalink(serverUrl, '', fileInfo.post_id, intl); + showPermalink(serverUrl, '', fileInfo.post_id); setAction('opening'); } }, [intl, serverUrl, fileInfo.post_id, setAction]); diff --git a/app/screens/login/index.tsx b/app/screens/login/index.tsx index a787bb1448..df2c3ca6e3 100644 --- a/app/screens/login/index.tsx +++ b/app/screens/login/index.tsx @@ -209,6 +209,7 @@ const LoginOptions = ({ {hasLoginForm &&
{ const {width} = useWindowDimensions(); const {slidesData} = useSlidesData(); @@ -73,7 +74,7 @@ const Onboarding = ({ // mark the onboarding as already viewed storeOnboardingViewedValue(); - goToScreen(Screens.SERVER, '', {animated: true, theme}, loginAnimationOptions()); + goToScreen(Screens.SERVER, '', {animated: true, theme, ...props}, loginAnimationOptions()); }, []); const nextSlide = useCallback(() => { diff --git a/app/screens/permalink/permalink.tsx b/app/screens/permalink/permalink.tsx index b0a57c6602..cab55a22e1 100644 --- a/app/screens/permalink/permalink.tsx +++ b/app/screens/permalink/permalink.tsx @@ -406,7 +406,8 @@ function Permalink({ function processThreadPosts(posts: PostModel[], postId: string) { posts.sort((a, b) => b.createAt - a.createAt); const postIndex = posts.findIndex((p) => p.id === postId); - return posts.slice(postIndex - POSTS_LIMIT, postIndex + POSTS_LIMIT + 1); + const start = postIndex - POSTS_LIMIT; + return posts.slice(start < 0 ? postIndex : start, postIndex + POSTS_LIMIT + 1); } export default Permalink; diff --git a/app/screens/server/index.tsx b/app/screens/server/index.tsx index 9431f9431a..c45feab09c 100644 --- a/app/screens/server/index.tsx +++ b/app/screens/server/index.tsx @@ -90,15 +90,16 @@ const Server = ({ const styles = getStyleSheet(theme); const {formatMessage} = intl; const disableServerUrl = Boolean(managedConfig?.allowOtherServers === 'false' && managedConfig?.serverUrl); + const additionalServer = launchType === Launch.AddServerFromDeepLink || launchType === Launch.AddServer; useEffect(() => { let serverName: string | undefined = defaultDisplayName || managedConfig?.serverName || LocalConfig.DefaultServerName; let serverUrl: string | undefined = defaultServerUrl || managedConfig?.serverUrl || LocalConfig.DefaultServerUrl; let autoconnect = managedConfig?.allowOtherServers === 'false' || LocalConfig.AutoSelectServerUrl; - if (launchType === Launch.DeepLink) { + if (launchType === Launch.DeepLink || launchType === Launch.AddServerFromDeepLink) { const deepLinkServerUrl = (extra as DeepLinkWithData).data?.serverUrl; - if (managedConfig) { + if (managedConfig.serverUrl) { autoconnect = (managedConfig.allowOtherServers === 'false' && managedConfig.serverUrl === deepLinkServerUrl); if (managedConfig.serverUrl !== deepLinkServerUrl || launchError) { Alert.alert('', intl.formatMessage({ @@ -343,11 +344,11 @@ const Server = ({ style={styles.flex} > { const onHandlePress = useCallback(async () => { await dismissBottomSheet(Screens.THREAD_OPTIONS); - showPermalink(serverUrl, '', threadId, intl); + showPermalink(serverUrl, '', threadId); }, [intl, serverUrl, threadId]); return ( diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts new file mode 100644 index 0000000000..61c03acf62 --- /dev/null +++ b/app/utils/deep_link/index.ts @@ -0,0 +1,183 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {createIntl, IntlShape} from 'react-intl'; +import urlParse from 'url-parse'; + +import {makeDirectChannel, switchToChannelByName} from '@actions/remote/channel'; +import {appEntry} from '@actions/remote/entry'; +import {showPermalink} from '@actions/remote/permalink'; +import {fetchUsersByUsernames} from '@actions/remote/user'; +import {DeepLink, Launch, Screens} from '@constants'; +import {getDefaultThemeByAppearance} from '@context/theme'; +import DatabaseManager from '@database/manager'; +import {DEFAULT_LOCALE, getTranslations} from '@i18n'; +import {getActiveServerUrl} from '@queries/app/servers'; +import {getCurrentUser, queryUsersByUsername} from '@queries/servers/user'; +import {dismissAllModalsAndPopToRoot, showModal} from '@screens/navigation'; +import EphemeralStore from '@store/ephemeral_store'; +import NavigationStore from '@store/navigation_store'; +import {errorBadChannel, errorUnkownUser} from '@utils/draft'; +import {logError} from '@utils/log'; +import {escapeRegex} from '@utils/markdown'; +import {addNewServer} from '@utils/server'; +import {removeProtocol} from '@utils/url'; + +import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkPlugin, DeepLinkWithData, LaunchProps} from '@typings/launch'; + +export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, location?: string) { + try { + const parsed = parseDeepLink(deepLinkUrl); + if (parsed.type === DeepLink.Invalid || !parsed.data || !parsed.data.serverUrl) { + return {error: true}; + } + + const currentServerUrl = await getActiveServerUrl(); + const existingServerUrl = DatabaseManager.searchUrl(parsed.data.serverUrl); + + // After checking the server for http & https then we add it + if (!existingServerUrl) { + const theme = EphemeralStore.theme || getDefaultThemeByAppearance(); + addNewServer(theme, parsed.data.serverUrl, undefined, parsed); + return {error: false}; + } + + if (existingServerUrl !== currentServerUrl && NavigationStore.getVisibleScreen()) { + await dismissAllModalsAndPopToRoot(); + DatabaseManager.setActiveServerDatabase(existingServerUrl); + appEntry(existingServerUrl, Date.now()); + await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME); + } + + const {database} = DatabaseManager.getServerDatabaseAndOperator(existingServerUrl); + const currentUser = await getCurrentUser(database); + const locale = currentUser?.locale || DEFAULT_LOCALE; + const intl = intlShape || createIntl({ + locale, + messages: getTranslations(locale), + }); + + switch (parsed.type) { + case DeepLink.Channel: { + const deepLinkData = parsed.data as DeepLinkChannel; + switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl); + break; + } + case DeepLink.DirectMessage: { + const deepLinkData = parsed.data as DeepLinkDM; + const userIds = await queryUsersByUsername(database, [deepLinkData.userName]).fetchIds(); + let userId = userIds.length ? userIds[0] : undefined; + if (!userId) { + const {users} = await fetchUsersByUsernames(existingServerUrl, [deepLinkData.userName], false); + if (users?.length) { + userId = users[0].id; + } + } + + if (userId) { + makeDirectChannel(existingServerUrl, userId, '', true); + } else { + errorUnkownUser(intl); + } + break; + } + case DeepLink.GroupMessage: { + const deepLinkData = parsed.data as DeepLinkGM; + switchToChannelByName(existingServerUrl, deepLinkData.channelId, deepLinkData.teamName, errorBadChannel, intl); + break; + } + case DeepLink.Permalink: { + const deepLinkData = parsed.data as DeepLinkPermalink; + if (NavigationStore.hasModalsOpened() || ![Screens.HOME, Screens.CHANNEL, Screens.GLOBAL_THREADS, Screens.THREAD].includes(NavigationStore.getVisibleScreen())) { + await dismissAllModalsAndPopToRoot(); + } + showPermalink(existingServerUrl, deepLinkData.teamName, deepLinkData.postId); + break; + } + case DeepLink.Plugin: { + const deepLinkData = parsed.data as DeepLinkPlugin; + showModal('PluginInternal', deepLinkData.id, {link: location}); + break; + } + } + return {error: false}; + } catch (error) { + logError('Failed to open channel from deeplink', error); + return {error: true}; + } +} + +export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { + const url = removeProtocol(deepLinkUrl); + + let match = new RegExp('(.*)\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url); + if (match) { + return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], channelName: match[3]}}; + } + + match = new RegExp('(.*)\\/([^\\/]+)\\/pl\\/(\\w+)').exec(url); + if (match) { + return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], postId: match[3]}}; + } + + match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/@(\\S+)').exec(url); + if (match) { + return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], userName: match[3]}}; + } + + match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/(\\S+)').exec(url); + if (match) { + return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], channelId: match[3]}}; + } + + match = new RegExp('(.*)\\/plugins\\/([^\\/]+)\\/(\\S+)').exec(url); + if (match) { + return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl: match[1], id: match[2], teamName: ''}}; + } + + return {type: DeepLink.Invalid, url: deepLinkUrl}; +} + +export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) { + if (!url || (!serverURL && !siteURL)) { + return ''; + } + + let urlToMatch = url; + const urlBase = serverURL || siteURL || ''; + + if (!url.startsWith('mattermost://')) { + // If url doesn't contain site or server URL, tack it on. + // e.g. URLs from autolink plugin. + const match = new RegExp(escapeRegex(urlBase)).exec(url); + if (!match) { + urlToMatch = urlBase + url; + } + } + + if (urlParse(urlToMatch).hostname === urlParse(urlBase).hostname) { + return urlToMatch; + } + + return ''; +} + +export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = false): LaunchProps => { + const parsed = parseDeepLink(deepLinkUrl); + const launchProps: LaunchProps = { + launchType: Launch.DeepLink, + coldStart, + }; + + switch (parsed.type) { + case DeepLink.Invalid: + launchProps.launchError = true; + break; + default: { + launchProps.extra = parsed; + break; + } + } + + return launchProps; +}; diff --git a/app/utils/server/index.ts b/app/utils/server/index.ts index f3130385a0..debff00faf 100644 --- a/app/utils/server/index.ts +++ b/app/utils/server/index.ts @@ -13,6 +13,7 @@ import {changeOpacity} from '@utils/theme'; import {tryOpenURL} from '@utils/url'; import type ServersModel from '@typings/database/models/app/servers'; +import type {DeepLinkWithData} from '@typings/launch'; export function isSupportedServer(currentVersion: string) { return isMinimumServerVersion(currentVersion, SupportedServer.MAJOR_VERSION, SupportedServer.MIN_VERSION, SupportedServer.PATCH_VERSION); @@ -39,15 +40,16 @@ export function semverFromServerVersion(value: string) { return `${major}.${minor}.${patch}`; } -export async function addNewServer(theme: Theme, serverUrl?: string, displayName?: string) { +export async function addNewServer(theme: Theme, serverUrl?: string, displayName?: string, deepLinkProps?: DeepLinkWithData) { await dismissBottomSheet(); const closeButtonId = 'close-server'; const props = { closeButtonId, displayName, - launchType: Launch.AddServer, + launchType: deepLinkProps ? Launch.AddServerFromDeepLink : Launch.AddServer, serverUrl, theme, + extra: deepLinkProps, }; const options = buildServerModalOptions(theme, closeButtonId); diff --git a/app/utils/url/index.ts b/app/utils/url/index.ts index 71d06da3ed..4208f2eeea 100644 --- a/app/utils/url/index.ts +++ b/app/utils/url/index.ts @@ -5,14 +5,11 @@ import GenericClient from '@mattermost/react-native-network-client'; import {Linking} from 'react-native'; import urlParse from 'url-parse'; -import {Files, DeepLink} from '@constants'; +import {Files} from '@constants'; import {emptyFunction} from '@utils/general'; -import {escapeRegex} from '@utils/markdown'; import {latinise} from './latinise'; -import type {DeepLinkWithData} from '@typings/launch'; - const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/; export function isValidUrl(url = '') { @@ -153,66 +150,6 @@ export function getScheme(url: string) { return match && match[1]; } -export const PERMALINK_GENERIC_TEAM_NAME_REDIRECT = '_redirect'; - -export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData { - const url = removeProtocol(deepLinkUrl); - - let match = new RegExp('(.*)\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url); - if (match) { - return {type: DeepLink.Channel, data: {serverUrl: match[1], teamName: match[2], channelName: match[3]}}; - } - - match = new RegExp('(.*)\\/([^\\/]+)\\/pl\\/(\\w+)').exec(url); - if (match) { - return {type: DeepLink.Permalink, data: {serverUrl: match[1], teamName: match[2], postId: match[3]}}; - } - - match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/@(\\S+)').exec(url); - if (match) { - return {type: DeepLink.DirectMessage, data: {serverUrl: match[1], teamName: match[2], userName: match[3]}}; - } - - match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/(\\S+)').exec(url); - if (match) { - return {type: DeepLink.GroupMessage, data: {serverUrl: match[1], teamName: match[2], channelId: match[3]}}; - } - - match = new RegExp('(.*)\\/plugins\\/([^\\/]+)\\/(\\S+)').exec(url); - if (match) { - return {type: DeepLink.Plugin, data: {serverUrl: match[1], id: match[2], teamName: ''}}; - } - - return {type: DeepLink.Invalid}; -} - -export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) { - if (!url || (!serverURL && !siteURL)) { - return null; - } - - let urlToMatch = url; - const urlBase = serverURL || siteURL || ''; - - if (!url.startsWith('mattermost://')) { - // If url doesn't contain site or server URL, tack it on. - // e.g. URLs from autolink plugin. - const match = new RegExp(escapeRegex(urlBase)).exec(url); - if (!match) { - urlToMatch = urlBase + url; - } - } - - if (urlParse(urlToMatch).hostname === urlParse(urlBase).hostname) { - const parsedDeepLink = parseDeepLink(urlToMatch); - if (parsedDeepLink.type !== DeepLink.Invalid) { - return parsedDeepLink; - } - } - - return null; -} - export function getYouTubeVideoId(link: string) { // https://youtube.com/watch?v= let match = (/youtube\.com\/watch\?\S*\bv=([a-zA-Z0-9_-]{6,11})/g).exec(link); diff --git a/app/utils/url/test.ts b/app/utils/url/test.ts index f1f5ee48d2..ba8f951b66 100644 --- a/app/utils/url/test.ts +++ b/app/utils/url/test.ts @@ -5,6 +5,7 @@ import {Linking} from 'react-native'; import DeepLinkType from '@constants/deep_linking'; import TestHelper from '@test/test_helper'; +import {matchDeepLink, parseDeepLink} from '@utils/deep_link'; import * as UrlUtils from '@utils/url'; /* eslint-disable max-nested-callbacks */ @@ -136,22 +137,22 @@ describe('UrlUtils', () => { { name: 'should return null if all inputs are empty', input: {url: '', serverURL: '', siteURL: ''}, - expected: null, + expected: {type: 'invalid'}, }, { name: 'should return null if any of the input is null', input: {url: '', serverURL: '', siteURL: null}, - expected: null, + expected: {type: 'invalid'}, }, { name: 'should return null if any of the input is null', input: {url: '', serverURL: null, siteURL: ''}, - expected: null, + expected: {type: 'invalid'}, }, { name: 'should return null if any of the input is null', input: {url: null, serverURL: '', siteURL: ''}, - expected: null, + expected: {type: 'invalid'}, }, { name: 'should return null for not supported link', @@ -160,12 +161,12 @@ describe('UrlUtils', () => { serverURL: SERVER_URL, siteURL: SITE_URL, }, - expected: null, + expected: {type: 'invalid'}, }, { name: 'should return null despite url subset match', input: {url: 'http://myserver.com', serverURL: 'http://myserver.co'}, - expected: null, + expected: {type: 'invalid'}, }, { name: 'should match despite no server URL in input link', @@ -253,7 +254,10 @@ describe('UrlUtils', () => { const {name, input, expected} = test; it(name, () => { - expect(UrlUtils.matchDeepLink(input.url!, input.serverURL!, input.siteURL!)).toEqual(expected); + const match = matchDeepLink(input.url!, input.serverURL!, input.siteURL!); + const parsed = parseDeepLink(match); + Reflect.deleteProperty(parsed, 'url'); + expect(parsed).toEqual(expected); }); } }); diff --git a/types/launch/index.ts b/types/launch/index.ts index 7033f126c3..1d4986cae0 100644 --- a/types/launch/index.ts +++ b/types/launch/index.ts @@ -32,6 +32,7 @@ export type DeepLinkType = typeof DeepLink[keyof typeof DeepLink]; export interface DeepLinkWithData { type: DeepLinkType; + url: string; data?: DeepLinkChannel | DeepLinkDM | DeepLinkGM | DeepLinkPermalink | DeepLinkPlugin; } From bd6c363d827a3ae3c9bf0f0adf0072ed1b92ba50 Mon Sep 17 00:00:00 2001 From: Avinash Lingaloo Date: Fri, 16 Dec 2022 20:59:40 +0400 Subject: [PATCH 03/71] Bump app build number to 444 (#6876) --- android/app/build.gradle | 2 +- ios/Mattermost.xcodeproj/project.pbxproj | 8 ++++---- ios/Mattermost/Info.plist | 2 +- ios/MattermostShare/Info.plist | 2 +- ios/NotificationService/Info.plist | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9a8df50dea..c3e125c65c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -145,7 +145,7 @@ android { applicationId "com.mattermost.rnbeta" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 443 + versionCode 444 versionName "2.0.0" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 7dc713ae8c..02f0ef2c02 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -1095,7 +1095,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 443; + CURRENT_PROJECT_VERSION = 444; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -1139,7 +1139,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 443; + CURRENT_PROJECT_VERSION = 444; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -1282,7 +1282,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 443; + CURRENT_PROJECT_VERSION = 444; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -1333,7 +1333,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 443; + CURRENT_PROJECT_VERSION = 444; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 5a74b2bbc3..b7ed0ea9f5 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -37,7 +37,7 @@ CFBundleVersion - 443 + 444 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index 145d5228a6..d738e7e41d 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 443 + 444 UIAppFonts OpenSans-Bold.ttf diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index e5f3b06f31..09ec58b325 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 443 + 444 NSExtension NSExtensionPointIdentifier From 4e7e37499d52481e77cb5fa97bb40bf63f01fb06 Mon Sep 17 00:00:00 2001 From: Yusuke Nemoto Date: Sat, 17 Dec 2022 03:09:47 +0900 Subject: [PATCH 04/71] Fix message id (#6875) * Fix message id Id `mobile.calls_call_thread` is conflicted with other message. This id should be `mobile.calls_open_channel`. * Update en.json --- app/products/calls/screens/call_screen/call_screen.tsx | 2 +- assets/base/i18n/en.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx index ad433ed5f4..bba482a043 100644 --- a/app/products/calls/screens/call_screen/call_screen.tsx +++ b/app/products/calls/screens/call_screen/call_screen.tsx @@ -273,7 +273,7 @@ const CallScreen = ({ const callThreadOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Call Thread'}); const recordOptionTitle = intl.formatMessage({id: 'mobile.calls_record', defaultMessage: 'Record'}); const stopRecordingOptionTitle = intl.formatMessage({id: 'mobile.calls_stop_recording', defaultMessage: 'Stop Recording'}); - const openChannelOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Open Channel'}); + const openChannelOptionTitle = intl.formatMessage({id: 'mobile.calls_open_channel', defaultMessage: 'Open Channel'}); useEffect(() => { mergeNavigationOptions('Call', { diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index b413c8c1a1..8bb68a8f19 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -369,7 +369,7 @@ "mobile.announcement_banner.title": "Announcement", "mobile.calls_call_ended": "Call ended", "mobile.calls_call_screen": "Call", - "mobile.calls_call_thread": "Open Channel", + "mobile.calls_call_thread": "Call Thread", "mobile.calls_current_call": "Current call", "mobile.calls_disable": "Disable calls", "mobile.calls_dismiss": "Dismiss", @@ -407,6 +407,7 @@ "mobile.calls_not_available_title": "Calls is not enabled", "mobile.calls_ok": "OK", "mobile.calls_okay": "Okay", + "mobile.calls_open_channel": "Open Channel", "mobile.calls_participant_limit_title_GA": "This call is at capacity", "mobile.calls_participant_rec": "The host has started recording this meeting. By staying in the meeting you give consent to being recorded.", "mobile.calls_participant_rec_title": "Recording is in progress", From 724d72d98a00fb94cbd6f1790a93131c243b3b64 Mon Sep 17 00:00:00 2001 From: Anurag Shivarathri Date: Sat, 17 Dec 2022 01:15:22 +0530 Subject: [PATCH 05/71] [MM-47483] Activity Indicator while loading thread posts (#6865) * Fix * Addressing feedback * Disabled pull to refresh when thread is being fetched * Test fail fix * Feedback changes --- app/actions/remote/post.ts | 5 ++ app/components/post_list/post_list.tsx | 7 ++- .../thread_overview/thread_overview.test.tsx | 2 + .../thread_overview/thread_overview.tsx | 55 ++++++++++++------- app/hooks/fetching_thread.ts | 22 ++++++++ .../thread_post_list/thread_post_list.tsx | 33 ++++++++--- app/store/fetching_thread_store.ts | 18 ++++++ assets/base/i18n/en.json | 1 + 8 files changed, 114 insertions(+), 29 deletions(-) create mode 100644 app/hooks/fetching_thread.ts create mode 100644 app/store/fetching_thread_store.ts diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index ac971708fc..9ead1cc40a 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -23,6 +23,7 @@ import {getPostById, getRecentPostsInChannel} from '@queries/servers/post'; import {getCurrentUserId, getCurrentChannelId} from '@queries/servers/system'; import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread'; import {queryAllUsers} from '@queries/servers/user'; +import {setFetchingThreadState} from '@store/fetching_thread_store'; import {getValidEmojis, matchEmoticons} from '@utils/emoji/helpers'; import {logError} from '@utils/log'; import {processPostsFetched} from '@utils/post'; @@ -583,6 +584,8 @@ export async function fetchPostThread(serverUrl: string, postId: string, options return {error}; } + setFetchingThreadState(postId, true); + try { const isCRTEnabled = await getIsCRTEnabled(operator.database); @@ -620,9 +623,11 @@ export async function fetchPostThread(serverUrl: string, postId: string, options } await operator.batchRecords(models); } + setFetchingThreadState(postId, false); return {posts: extractRecordsForTable(posts, MM_TABLES.SERVER.POST)}; } catch (error) { forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); + setFetchingThreadState(postId, false); return {error}; } } diff --git a/app/components/post_list/post_list.tsx b/app/components/post_list/post_list.tsx index e62d3daab4..003529fcb6 100644 --- a/app/components/post_list/post_list.tsx +++ b/app/components/post_list/post_list.tsx @@ -30,6 +30,7 @@ type Props = { currentTimezone: string | null; currentUserId: string; currentUsername: string; + disablePullToRefresh?: boolean; highlightedId?: PostModel['id']; highlightPinnedOrSaved?: boolean; isCRTEnabled?: boolean; @@ -45,6 +46,7 @@ type Props = { showMoreMessages?: boolean; showNewMessageLine?: boolean; footer?: ReactElement; + header?: ReactElement; testID: string; currentCallBarVisible?: boolean; joinCallBannerVisible?: boolean; @@ -84,7 +86,9 @@ const PostList = ({ currentTimezone, currentUserId, currentUsername, + disablePullToRefresh, footer, + header, highlightedId, highlightPinnedOrSaved = true, isCRTEnabled, @@ -365,7 +369,7 @@ const PostList = ({ return ( <> { const props = { isSaved: true, repliesCount: 0, + rootId: '', rootPost: {} as PostModel, testID: 'thread-overview', }; @@ -26,6 +27,7 @@ describe('ThreadOverview', () => { const props = { isSaved: false, repliesCount: 2, + rootId: '', rootPost: {} as PostModel, testID: 'thread-overview', }; diff --git a/app/components/post_list/thread_overview/thread_overview.tsx b/app/components/post_list/thread_overview/thread_overview.tsx index 2a80341e06..f9f532e9ca 100644 --- a/app/components/post_list/thread_overview/thread_overview.tsx +++ b/app/components/post_list/thread_overview/thread_overview.tsx @@ -13,6 +13,7 @@ import {Screens} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; +import {useFetchingThreadState} from '@hooks/fetching_thread'; import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -23,6 +24,7 @@ import type PostModel from '@typings/database/models/servers/post'; type Props = { isSaved: boolean; repliesCount: number; + rootId: string; rootPost?: PostModel; testID: string; style?: StyleProp; @@ -56,13 +58,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }; }); -const ThreadOverview = ({isSaved, repliesCount, rootPost, style, testID}: Props) => { +const ThreadOverview = ({isSaved, repliesCount, rootId, rootPost, style, testID}: Props) => { const theme = useTheme(); const styles = getStyleSheet(theme); const intl = useIntl(); const isTablet = useIsTablet(); const serverUrl = useServerUrl(); + const isFetchingThread = useFetchingThreadState(rootId); const onHandleSavePress = useCallback(preventDoubleTap(() => { if (rootPost?.id) { @@ -98,30 +101,44 @@ const ThreadOverview = ({isSaved, repliesCount, rootPost, style, testID}: Props) const saveButtonTestId = isSaved ? `${testID}.unsave.button` : `${testID}.save.button`; + let repliesCountElement; + if (repliesCount > 0) { + repliesCountElement = ( + + ); + } else if (isFetchingThread) { + repliesCountElement = ( + + ); + } else { + repliesCountElement = ( + + ); + } + return ( - { - repliesCount > 0 ? ( - - ) : ( - - ) - } + {repliesCountElement} { + const [isFetching, setIsFetching] = useState(false); + useEffect(() => { + const sub = subject.pipe( + switchMap((s) => of$(s[rootId] || false)), + distinctUntilChanged(), + ).subscribe(setIsFetching); + + return () => sub.unsubscribe(); + }, []); + + return isFetching; +}; diff --git a/app/screens/thread/thread_post_list/thread_post_list.tsx b/app/screens/thread/thread_post_list/thread_post_list.tsx index 86cdda67e4..e30b11adba 100644 --- a/app/screens/thread/thread_post_list/thread_post_list.tsx +++ b/app/screens/thread/thread_post_list/thread_post_list.tsx @@ -2,16 +2,19 @@ // See LICENSE.txt for license information. import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import {StyleSheet, View} from 'react-native'; +import {ActivityIndicator, StyleSheet, View} from 'react-native'; import {Edge, SafeAreaView} from 'react-native-safe-area-context'; import {fetchPostThread} from '@actions/remote/post'; import {markThreadAsRead} from '@actions/remote/thread'; +import {PER_PAGE_DEFAULT} from '@client/rest/constants'; import PostList from '@components/post_list'; import {Screens} from '@constants'; import {useServerUrl} from '@context/server'; +import {useTheme} from '@context/theme'; import {debounce} from '@helpers/api/general'; import {useIsTablet} from '@hooks/device'; +import {useFetchingThreadState} from '@hooks/fetching_thread'; import {isMinimumServerVersion} from '@utils/helpers'; import type PostModel from '@typings/database/models/servers/post'; @@ -42,23 +45,28 @@ const ThreadPostList = ({ }: Props) => { const isTablet = useIsTablet(); const serverUrl = useServerUrl(); + const theme = useTheme(); + const isFetchingThread = useFetchingThreadState(rootPost.id); - const canLoadPosts = useRef(true); - const fetchingPosts = useRef(false); + const canLoadMorePosts = useRef(true); const onEndReached = useCallback(debounce(async () => { - if (isMinimumServerVersion(version || '', 6, 7) && !fetchingPosts.current && canLoadPosts.current && posts.length) { - fetchingPosts.current = true; - const options: FetchPaginatedThreadOptions = {}; + if (isMinimumServerVersion(version || '', 6, 7) && !isFetchingThread && canLoadMorePosts.current && posts.length) { + const options: FetchPaginatedThreadOptions = { + perPage: PER_PAGE_DEFAULT, + }; const lastPost = posts[posts.length - 1]; if (lastPost) { options.fromPost = lastPost.id; options.fromCreateAt = lastPost.createAt; } const result = await fetchPostThread(serverUrl, rootPost.id, options); - fetchingPosts.current = false; - canLoadPosts.current = Boolean(result?.posts?.length); + + // Root post is always fetched, so the result would include +1 + canLoadMorePosts.current = (result?.posts?.length || 0) > PER_PAGE_DEFAULT; + } else { + canLoadMorePosts.current = false; } - }, 500), [rootPost, posts, version]); + }, 500), [isFetchingThread, rootPost, posts, version]); const threadPosts = useMemo(() => { return [...posts, rootPost]; @@ -82,10 +90,16 @@ const ThreadPostList = ({ const lastViewedAt = isCRTEnabled ? (thread?.viewedAt ?? 0) : channelLastViewedAt; + let header; + if (isFetchingThread && threadPosts.length === 1) { + header = ; + } + const postList = ( } testID='thread.post_list' /> diff --git a/app/store/fetching_thread_store.ts b/app/store/fetching_thread_store.ts new file mode 100644 index 0000000000..712ea7aaf7 --- /dev/null +++ b/app/store/fetching_thread_store.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {BehaviorSubject} from 'rxjs'; + +type State = {[id: string]: boolean}; + +const defaultState: State = {}; + +export const subject: BehaviorSubject = new BehaviorSubject(defaultState); + +export const setFetchingThreadState = (rootId: string, isFetching: boolean) => { + const prevState = subject.value; + subject.next({ + ...prevState, + [rootId]: isFetching, + }); +}; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 8bb68a8f19..de12f0e01d 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -895,6 +895,7 @@ "terms_of_service.title": "Terms of Service", "thread.header.thread": "Thread", "thread.header.thread_in": "in {channelName}", + "thread.loadingReplies": "Loading replies...", "thread.noReplies": "No replies yet", "thread.options.title": "Thread Actions", "thread.repliesCount": "{repliesCount, number} {repliesCount, plural, one {reply} other {replies}}", From 63e6b0f6264c865a5cf178253ad61e67633fb11c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Mon, 19 Dec 2022 10:32:05 +0100 Subject: [PATCH 06/71] Fix MM46418 (#6796) --- app/queries/servers/user.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/app/queries/servers/user.ts b/app/queries/servers/user.ts index 3279ec1177..f8ff40b2e9 100644 --- a/app/queries/servers/user.ts +++ b/app/queries/servers/user.ts @@ -2,14 +2,12 @@ // See LICENSE.txt for license information. import {Database, Q} from '@nozbe/watermelondb'; -import {combineLatest, Observable, of as of$} from 'rxjs'; +import {combineLatest, of as of$} from 'rxjs'; import {distinctUntilChanged, switchMap} from 'rxjs/operators'; import {Preferences} from '@constants'; import {MM_TABLES} from '@constants/database'; import {getTeammateNameDisplaySetting} from '@helpers/api/preference'; -import {observeMyChannel} from '@queries/servers/channel'; -import {isChannelAdmin} from '@utils/user'; import {queryPreferencesByCategoryAndName} from './preference'; import {observeCurrentUserId, observeLicense, getCurrentUserId, getConfig, getLicense, observeConfigValue} from './system'; @@ -106,24 +104,17 @@ export const observeUserIsTeamAdmin = (database: Database, userId: string, teamI const id = `${teamId}-${userId}`; return database.get(TEAM_MEMBERSHIP).query( Q.where('id', Q.eq(id)), - ).observe().pipe( + ).observeWithColumns(['scheme_admin']).pipe( switchMap((tm) => of$(tm.length ? tm[0].schemeAdmin : false)), ); }; export const observeUserIsChannelAdmin = (database: Database, userId: string, channelId: string) => { const id = `${channelId}-${userId}`; - const myChannelRoles = observeMyChannel(database, channelId).pipe( - switchMap((mc) => of$(mc?.roles || '')), - distinctUntilChanged(), - ); - const channelSchemeAdmin = database.get(CHANNEL_MEMBERSHIP).query( + return database.get(CHANNEL_MEMBERSHIP).query( Q.where('id', Q.eq(id)), - ).observe().pipe( + ).observeWithColumns(['scheme_admin']).pipe( switchMap((cm) => of$(cm.length ? cm[0].schemeAdmin : false)), distinctUntilChanged(), ); - return combineLatest([myChannelRoles, channelSchemeAdmin]).pipe( - switchMap(([mcr, csa]) => of$(isChannelAdmin(mcr) || csa)), - ) as Observable; }; From 191a6400072b5f7c36c053e2cf1f17646f1f8dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Mon, 19 Dec 2022 12:25:23 +0100 Subject: [PATCH 07/71] Show loading only when team channels are being loaded (#6872) * Show loading only when team channels are being loaded * Fix tests * Remove unneeded event * Refactor into using hooks --- app/actions/remote/channel.ts | 8 ++++ app/actions/remote/entry/app.ts | 4 ++ app/actions/remote/entry/common.ts | 7 +-- app/actions/remote/entry/gql_common.ts | 7 +-- app/actions/remote/entry/login.ts | 4 ++ app/actions/remote/entry/notification.ts | 4 ++ app/actions/remote/post.ts | 11 +---- app/actions/remote/team.ts | 13 ++++++ app/actions/websocket/index.ts | 16 +++---- app/actions/websocket/teams.ts | 43 +++++++++++-------- app/constants/events.ts | 1 - app/hooks/teams_loading.ts | 24 +++++++++++ .../__snapshots__/index.test.tsx.snap | 34 --------------- .../categories_list/categories/error.tsx | 4 ++ .../header/__snapshots__/header.test.tsx.snap | 34 --------------- .../header/loading_unreads.tsx | 20 +++------ .../load_channel_error.tsx | 4 ++ .../load_teams_error/index.tsx | 4 ++ app/screens/navigation.ts | 1 - app/store/team_load_store.ts | 17 ++++++++ 20 files changed, 125 insertions(+), 135 deletions(-) create mode 100644 app/hooks/teams_loading.ts create mode 100644 app/store/team_load_store.ts diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 780ebf190d..6c312e921c 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -24,6 +24,7 @@ import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, r import {getCurrentUser} from '@queries/servers/user'; import {dismissAllModals, popToRoot} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; +import {setTeamLoading} from '@store/team_load_store'; import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel'; import {isTablet} from '@utils/helpers'; import {logDebug, logError, logInfo} from '@utils/log'; @@ -359,6 +360,9 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string, } try { + if (!fetchOnly) { + setTeamLoading(serverUrl, true); + } const [allChannels, channelMemberships, categoriesWithOrder] = await Promise.all([ client.getMyChannels(teamId, includeDeleted, since), client.getMyChannelMembers(teamId), @@ -387,10 +391,14 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string, if (models.length) { await operator.batchRecords(models); } + setTeamLoading(serverUrl, false); } return {channels, memberships, categories}; } catch (error) { + if (!fetchOnly) { + setTeamLoading(serverUrl, false); + } forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); return {error}; } diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index 1339bdaaf2..a68e9dafeb 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -6,6 +6,7 @@ import {fetchConfigAndLicense} from '@actions/remote/systems'; import DatabaseManager from '@database/manager'; import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system'; import {getCurrentUser} from '@queries/servers/user'; +import {setTeamLoading} from '@store/team_load_store'; import {deleteV1Data} from '@utils/file'; import {logInfo} from '@utils/log'; @@ -37,8 +38,10 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) const currentChannelId = await getCurrentChannelId(database); const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since; + setTeamLoading(serverUrl, true); const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since); if ('error' in entryData) { + setTeamLoading(serverUrl, false); return {error: entryData.error}; } @@ -55,6 +58,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) const dt = Date.now(); await operator.batchRecords(models); logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`); + setTeamLoading(serverUrl, false); const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!; const config = await getConfig(database); diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index a6dc2ff587..3fae907da5 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import {Database, Model} from '@nozbe/watermelondb'; -import {DeviceEventEmitter} from 'react-native'; import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel'; import {fetchGroupsForMember} from '@actions/remote/groups'; @@ -14,7 +13,7 @@ import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKic import {syncTeamThreads} from '@actions/remote/thread'; import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user'; import {gqlAllChannels} from '@client/graphQL/entry'; -import {Events, General, Preferences, Screens} from '@constants'; +import {General, Preferences, Screens} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; import DatabaseManager from '@database/manager'; @@ -320,9 +319,7 @@ export async function restDeferredAppEntryActions( channelsToFetchProfiles = new Set(directChannels); // defer fetching posts for unread channels on initial team - fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, true); - } else { - DeviceEventEmitter.emit(Events.FETCHING_POSTS, false); + fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId); } }, FETCH_UNREADS_TIMEOUT); diff --git a/app/actions/remote/entry/gql_common.ts b/app/actions/remote/entry/gql_common.ts index 8d11a2012d..b2db28b107 100644 --- a/app/actions/remote/entry/gql_common.ts +++ b/app/actions/remote/entry/gql_common.ts @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import {Database} from '@nozbe/watermelondb'; -import {DeviceEventEmitter} from 'react-native'; import {storeConfigAndLicense} from '@actions/local/systems'; import {MyChannelsRequest} from '@actions/remote/channel'; @@ -12,7 +11,7 @@ import {MyTeamsRequest} from '@actions/remote/team'; import {syncTeamThreads} from '@actions/remote/thread'; import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user'; import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry'; -import {Events, Preferences} from '@constants'; +import {Preferences} from '@constants'; import DatabaseManager from '@database/manager'; import {getPreferenceValue} from '@helpers/api/preference'; import {selectDefaultTeam} from '@helpers/api/team'; @@ -49,9 +48,7 @@ export async function deferredAppEntryGraphQLActions( setTimeout(() => { if (chData?.channels?.length && chData.memberships?.length) { // defer fetching posts for unread channels on initial team - fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, true); - } else { - DeviceEventEmitter.emit(Events.FETCHING_POSTS, false); + fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId); } }, FETCH_UNREADS_TIMEOUT); diff --git a/app/actions/remote/entry/login.ts b/app/actions/remote/entry/login.ts index dd141f0d36..751bafe0c6 100644 --- a/app/actions/remote/entry/login.ts +++ b/app/actions/remote/entry/login.ts @@ -6,6 +6,7 @@ import {fetchConfigAndLicense} from '@actions/remote/systems'; import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; import {setCurrentTeamAndChannelId} from '@queries/servers/system'; +import {setTeamLoading} from '@store/team_load_store'; import {isTablet} from '@utils/helpers'; import {deferredAppEntryActions, entry} from './gql_common'; @@ -47,9 +48,11 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs) return {error: clData.error}; } + setTeamLoading(serverUrl, true); const entryData = await entry(serverUrl, '', ''); if ('error' in entryData) { + setTeamLoading(serverUrl, false); return {error: entryData.error}; } @@ -66,6 +69,7 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs) } await operator.batchRecords(models); + setTeamLoading(serverUrl, false); const config = clData.config || {} as ClientConfig; const license = clData.license || {} as ClientLicense; diff --git a/app/actions/remote/entry/notification.ts b/app/actions/remote/entry/notification.ts index 25eb4bb731..76ef6ee381 100644 --- a/app/actions/remote/entry/notification.ts +++ b/app/actions/remote/entry/notification.ts @@ -14,6 +14,7 @@ import {getIsCRTEnabled} from '@queries/servers/thread'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; +import {setTeamLoading} from '@store/team_load_store'; import {isTablet} from '@utils/helpers'; import {emitNotificationError} from '@utils/notification'; import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme'; @@ -81,8 +82,10 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not switchedToScreen = true; } + setTeamLoading(serverUrl, true); const entryData = await entry(serverUrl, teamId, channelId); if ('error' in entryData) { + setTeamLoading(serverUrl, false); return {error: entryData.error}; } const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; @@ -134,6 +137,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not } await operator.batchRecords(models); + setTeamLoading(serverUrl, false); const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!; const config = await getConfig(database); diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index 9ead1cc40a..6adf5acfee 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -326,12 +326,9 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string, } } -export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string, emitEvent = false) => { +export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string) => { try { const promises = []; - if (emitEvent) { - DeviceEventEmitter.emit(Events.FETCHING_POSTS, true); - } for (const member of memberships) { const channel = channels.find((c) => c.id === member.channel_id); if (channel && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) { @@ -339,13 +336,7 @@ export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: C } } await Promise.all(promises); - if (emitEvent) { - DeviceEventEmitter.emit(Events.FETCHING_POSTS, false); - } } catch (error) { - if (emitEvent) { - DeviceEventEmitter.emit(Events.FETCHING_POSTS, false); - } return {error}; } diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index a00a83b09d..c23ece7cb6 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -15,6 +15,7 @@ import {prepareCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@qu import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, getNthLastChannelFromTeam, queryTeamsById, syncTeamTable, getLastTeam, getTeamById, removeTeamFromTeamHistory} from '@queries/servers/team'; import {dismissAllModals, popToRoot, resetToTeams} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; +import {setTeamLoading} from '@store/team_load_store'; import {isTablet} from '@utils/helpers'; import {logDebug} from '@utils/log'; @@ -56,12 +57,16 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s return {error}; } + let loadEventSent = false; try { EphemeralStore.startAddingToTeam(teamId); const team = await client.getTeam(teamId); const member = await client.addToTeam(teamId, userId); if (!fetchOnly) { + setTeamLoading(serverUrl, true); + loadEventSent = true; + fetchRolesIfNeeded(serverUrl, member.roles.split(' ')); const {channels, memberships: channelMembers, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true); const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; @@ -80,6 +85,8 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s ])).flat(); await operator.batchRecords(models); + setTeamLoading(serverUrl, false); + loadEventSent = false; if (await isTablet()) { const channel = await getDefaultChannelForTeam(operator.database, teamId); @@ -87,11 +94,17 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s fetchPostsForChannel(serverUrl, channel.id); } } + } else { + setTeamLoading(serverUrl, false); + loadEventSent = false; } } EphemeralStore.finishAddingToTeam(teamId); return {member}; } catch (error) { + if (loadEventSent) { + setTeamLoading(serverUrl, false); + } EphemeralStore.finishAddingToTeam(teamId); forceLogoutIfNecessary(serverUrl, error as ClientError); return {error}; diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 23e6f0d746..5f094dbd37 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -1,8 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {DeviceEventEmitter} from 'react-native'; - import {markChannelAsViewed} from '@actions/local/channel'; import {markChannelAsRead} from '@actions/remote/channel'; import {handleEntryAfterLoadNavigation} from '@actions/remote/entry/common'; @@ -30,11 +28,10 @@ import { handleCallUserVoiceOn, } from '@calls/connection/websocket_event_handlers'; import {isSupportedServerCalls} from '@calls/utils'; -import {Events, Screens, WebsocketEvents} from '@constants'; +import {Screens, WebsocketEvents} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; import AppsManager from '@managers/apps_manager'; -import {getActiveServerUrl} from '@queries/app/servers'; import {getCurrentChannel} from '@queries/servers/channel'; import {getLastPostInThread} from '@queries/servers/post'; import { @@ -50,6 +47,7 @@ import {getIsCRTEnabled} from '@queries/servers/thread'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; +import {setTeamLoading} from '@store/team_load_store'; import {isTablet} from '@utils/helpers'; import {logDebug, logInfo} from '@utils/log'; @@ -141,15 +139,10 @@ async function doReconnect(serverUrl: string) { const currentTeam = await getCurrentTeam(database); const currentChannel = await getCurrentChannel(database); - const currentActiveServerUrl = await getActiveServerUrl(); - if (serverUrl === currentActiveServerUrl) { - DeviceEventEmitter.emit(Events.FETCHING_POSTS, true); - } + setTeamLoading(serverUrl, true); const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt); if ('error' in entryData) { - if (serverUrl === currentActiveServerUrl) { - DeviceEventEmitter.emit(Events.FETCHING_POSTS, false); - } + setTeamLoading(serverUrl, false); return; } const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; @@ -159,6 +152,7 @@ async function doReconnect(serverUrl: string) { const dt = Date.now(); await operator.batchRecords(models); logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`); + setTeamLoading(serverUrl, false); await fetchPostDataIfNeeded(serverUrl); diff --git a/app/actions/websocket/teams.ts b/app/actions/websocket/teams.ts index cf5b501839..3602f94cee 100644 --- a/app/actions/websocket/teams.ts +++ b/app/actions/websocket/teams.ts @@ -14,6 +14,7 @@ import {prepareMyChannelsForTeam} from '@queries/servers/channel'; import {getCurrentTeam, prepareMyTeams} from '@queries/servers/team'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; +import {setTeamLoading} from '@store/team_load_store'; import {logDebug} from '@utils/log'; export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMessage) { @@ -62,10 +63,6 @@ export async function handleUpdateTeamEvent(serverUrl: string, msg: WebSocketMes } export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSocketMessage) { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { - return; - } const {team_id: teamId} = msg.data; // Ignore duplicated team join events sent by the server @@ -74,24 +71,32 @@ export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSock } EphemeralStore.startAddingToTeam(teamId); - const {teams, memberships: teamMemberships} = await fetchMyTeam(serverUrl, teamId, true); + try { + setTeamLoading(serverUrl, true); + const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const {teams, memberships: teamMemberships} = await fetchMyTeam(serverUrl, teamId, true); - const modelPromises: Array> = []; - if (teams?.length && teamMemberships?.length) { - const {channels, memberships, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true); - modelPromises.push(prepareCategoriesAndCategoriesChannels(operator, categories || [], true)); - modelPromises.push(...await prepareMyChannelsForTeam(operator, teamId, channels || [], memberships || [])); + const modelPromises: Array> = []; + if (teams?.length && teamMemberships?.length) { + const {channels, memberships, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true); + modelPromises.push(prepareCategoriesAndCategoriesChannels(operator, categories || [], true)); + modelPromises.push(...await prepareMyChannelsForTeam(operator, teamId, channels || [], memberships || [])); - const {roles} = await fetchRoles(serverUrl, teamMemberships, memberships, undefined, true); - modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true})); + const {roles} = await fetchRoles(serverUrl, teamMemberships, memberships, undefined, true); + modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true})); + } + + if (teams && teamMemberships) { + modelPromises.push(...prepareMyTeams(operator, teams, teamMemberships)); + } + + const models = await Promise.all(modelPromises); + await operator.batchRecords(models.flat()); + setTeamLoading(serverUrl, false); + } catch (error) { + logDebug('could not handle user added to team websocket event'); + setTeamLoading(serverUrl, false); } - if (teams && teamMemberships) { - modelPromises.push(...prepareMyTeams(operator, teams, teamMemberships)); - } - - const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); - EphemeralStore.finishAddingToTeam(teamId); } diff --git a/app/constants/events.ts b/app/constants/events.ts index 0dc6c1afc0..5d34955cef 100644 --- a/app/constants/events.ts +++ b/app/constants/events.ts @@ -9,7 +9,6 @@ export default keyMirror({ CHANNEL_SWITCH: null, CLOSE_BOTTOM_SHEET: null, CONFIG_CHANGED: null, - FETCHING_POSTS: null, FREEZE_SCREEN: null, GALLERY_ACTIONS: null, LEAVE_CHANNEL: null, diff --git a/app/hooks/teams_loading.ts b/app/hooks/teams_loading.ts new file mode 100644 index 0000000000..661282bd8a --- /dev/null +++ b/app/hooks/teams_loading.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {useEffect, useState} from 'react'; +import {of as of$} from 'rxjs'; +import {switchMap, distinctUntilChanged} from 'rxjs/operators'; + +import {getLoadingTeamChannelsSubject} from '@store/team_load_store'; + +export const useTeamsLoading = (serverUrl: string) => { + // const subject = getLoadingTeamChannelsSubject(serverUrl); + // const [loading, setLoading] = useState(subject.getValue() !== 0); + const [loading, setLoading] = useState(false); + useEffect(() => { + const sub = getLoadingTeamChannelsSubject(serverUrl).pipe( + switchMap((v) => of$(v !== 0)), + distinctUntilChanged(), + ).subscribe(setLoading); + + return () => sub.unsubscribe(); + }, []); + + return loading; +}; diff --git a/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap b/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap index 74f6b532fe..f9a4925f82 100644 --- a/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap +++ b/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap @@ -144,40 +144,6 @@ exports[`components/categories_list should render channels error 1`] = ` } testID="channel_list_header.server_display_name" /> - { const {formatMessage} = useIntl(); @@ -16,7 +17,10 @@ const LoadCategoriesError = () => { const onRetryTeams = useCallback(async () => { setLoading(true); + + setTeamLoading(serverUrl, true); const {error} = await retryInitialTeamAndChannel(serverUrl); + setTeamLoading(serverUrl, false); if (error) { setLoading(false); diff --git a/app/screens/home/channel_list/categories_list/header/__snapshots__/header.test.tsx.snap b/app/screens/home/channel_list/categories_list/header/__snapshots__/header.test.tsx.snap index e2ee64617b..a016dddf93 100644 --- a/app/screens/home/channel_list/categories_list/header/__snapshots__/header.test.tsx.snap +++ b/app/screens/home/channel_list/categories_list/header/__snapshots__/header.test.tsx.snap @@ -124,40 +124,6 @@ exports[`components/channel_list/header Channel List Header Component should mat } testID="channel_list_header.server_display_name" /> - `; diff --git a/app/screens/home/channel_list/categories_list/header/loading_unreads.tsx b/app/screens/home/channel_list/categories_list/header/loading_unreads.tsx index bdd3378855..ae1fd0ae1b 100644 --- a/app/screens/home/channel_list/categories_list/header/loading_unreads.tsx +++ b/app/screens/home/channel_list/categories_list/header/loading_unreads.tsx @@ -1,12 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useEffect, useState} from 'react'; -import {DeviceEventEmitter} from 'react-native'; +import React, {useEffect} from 'react'; import Animated, {cancelAnimation, Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated'; -import {Events} from '@constants'; +import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; +import {useTeamsLoading} from '@hooks/teams_loading'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ @@ -26,9 +26,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ const LoadingUnreads = () => { const theme = useTheme(); const style = getStyleSheet(theme); - const [loading, setLoading] = useState(true); const opacity = useSharedValue(1); const rotation = useSharedValue(0); + const serverUrl = useServerUrl(); + const loading = useTeamsLoading(serverUrl); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value, @@ -52,17 +53,6 @@ const LoadingUnreads = () => { }; }, [loading]); - useEffect(() => { - const listener = DeviceEventEmitter.addListener(Events.FETCHING_POSTS, (value: boolean) => { - setLoading(value); - if (value) { - rotation.value = 0; - } - }); - - return () => listener.remove(); - }, []); - if (!loading) { return null; } diff --git a/app/screens/home/channel_list/categories_list/load_channels_error/load_channel_error.tsx b/app/screens/home/channel_list/categories_list/load_channels_error/load_channel_error.tsx index c89c45346f..50cac10f7a 100644 --- a/app/screens/home/channel_list/categories_list/load_channels_error/load_channel_error.tsx +++ b/app/screens/home/channel_list/categories_list/load_channels_error/load_channel_error.tsx @@ -7,6 +7,7 @@ import {useIntl} from 'react-intl'; import {retryInitialChannel} from '@actions/remote/retry'; import LoadingError from '@components/loading_error'; import {useServerUrl} from '@context/server'; +import {setTeamLoading} from '@store/team_load_store'; import LoadTeamsError from '../load_teams_error'; @@ -22,7 +23,10 @@ const LoadChannelsError = ({teamDisplayName, teamId}: Props) => { const onRetryTeams = useCallback(async () => { setLoading(true); + + setTeamLoading(serverUrl, true); const {error} = await retryInitialChannel(serverUrl, teamId); + setTeamLoading(serverUrl, false); if (error) { setLoading(false); diff --git a/app/screens/home/channel_list/categories_list/load_teams_error/index.tsx b/app/screens/home/channel_list/categories_list/load_teams_error/index.tsx index 953cc52967..82f6a21231 100644 --- a/app/screens/home/channel_list/categories_list/load_teams_error/index.tsx +++ b/app/screens/home/channel_list/categories_list/load_teams_error/index.tsx @@ -7,6 +7,7 @@ import {useIntl} from 'react-intl'; import {retryInitialTeamAndChannel} from '@actions/remote/retry'; import LoadingError from '@components/loading_error'; import {useServerDisplayName, useServerUrl} from '@context/server'; +import {setTeamLoading} from '@store/team_load_store'; const LoadTeamsError = () => { const {formatMessage} = useIntl(); @@ -16,7 +17,10 @@ const LoadTeamsError = () => { const onRetryTeams = useCallback(async () => { setLoading(true); + + setTeamLoading(serverUrl, true); const {error} = await retryInitialTeamAndChannel(serverUrl); + setTeamLoading(serverUrl, false); if (error) { setLoading(false); diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index eb93f223e6..e8b1882097 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -252,7 +252,6 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal} dismissModal({componentId: Screens.LOGIN}); dismissModal({componentId: Screens.SSO}); dismissModal({componentId: Screens.BOTTOM_SHEET}); - DeviceEventEmitter.emit(Events.FETCHING_POSTS, false); if (passProps.launchType === Launch.AddServerFromDeepLink) { Navigation.updateProps(Screens.HOME, {launchType: Launch.DeepLink, extra: passProps.extra}); } diff --git a/app/store/team_load_store.ts b/app/store/team_load_store.ts new file mode 100644 index 0000000000..1721eb5b16 --- /dev/null +++ b/app/store/team_load_store.ts @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {BehaviorSubject} from 'rxjs'; + +const loadingTeamChannels: {[serverUrl: string]: BehaviorSubject} = {}; + +export const getLoadingTeamChannelsSubject = (serverUrl: string) => { + if (!loadingTeamChannels[serverUrl]) { + loadingTeamChannels[serverUrl] = new BehaviorSubject(0); + } + return loadingTeamChannels[serverUrl]; +}; + +export const setTeamLoading = (serverUrl: string, loading: boolean) => { + const subject = getLoadingTeamChannelsSubject(serverUrl); + subject.next(subject.value + (loading ? 1 : -1)); +}; From a231b72079074b1e49034a1067f28f88fed28f00 Mon Sep 17 00:00:00 2001 From: kaakaa Date: Fri, 16 Dec 2022 14:24:52 +0000 Subject: [PATCH 08/71] Translated using Weblate (Japanese) Currently translated at 94.6% (881 of 931 strings) Translation: mattermost-languages-shipped/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/ja/ --- assets/base/i18n/ja.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/base/i18n/ja.json b/assets/base/i18n/ja.json index 30a88f18f9..45f0a79d6a 100644 --- a/assets/base/i18n/ja.json +++ b/assets/base/i18n/ja.json @@ -872,5 +872,13 @@ "invite_people_to_team.message": "Mattermostでコラボレーションやコミュニケーションを行うためのリンクはこちらです。", "integration_selector.multiselect.submit": "完了", "announcment_banner.okay": "OK", - "announcment_banner.dismiss": "アナウンスを閉じる" + "announcment_banner.dismiss": "アナウンスを閉じる", + "general_settings.report_problem": "問題を報告する", + "extension.no_servers.title": "どのサーバーにも接続していません", + "extension.no_servers.description": "コンテンツを共有するには、Mattermostサーバーにログインする必要があります。", + "extension.no_memberships.title": "まだどのチームにも所属していません", + "extension.no_memberships.description": "コンテンツを共有するには、Mattermostサーバー上のメンバになる必要があります。", + "connection_banner.not_reachable": "サーバーに到達できません", + "connection_banner.not_connected": "インターネットに接続していません", + "connection_banner.connected": "接続が回復しました" } From a72cae8ff2dbb895cd986a2be16191bd037eeadf Mon Sep 17 00:00:00 2001 From: jprusch Date: Fri, 16 Dec 2022 23:45:00 +0000 Subject: [PATCH 09/71] Translated using Weblate (German) Currently translated at 100.0% (933 of 933 strings) Translation: mattermost-languages-shipped/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/de/ --- assets/base/i18n/de.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/base/i18n/de.json b/assets/base/i18n/de.json index cb0f38fc73..56a74a9400 100644 --- a/assets/base/i18n/de.json +++ b/assets/base/i18n/de.json @@ -896,7 +896,7 @@ "mobile.calls_host": "Gastgeber", "mobile.calls_host_rec": "Du zeichnest diese Besprechung auf. Bitte informiere alle Teilnehmer darüber, dass diese Sitzung aufgezeichnet wird.", "mobile.calls_dismiss": "Verwerfen", - "mobile.calls_call_thread": "Kanal öffnen", + "mobile.calls_call_thread": "Call Unterhaltung", "mobile.calls_stop_recording": "Aufnahme stoppen", "mobile.calls_record": "Aufnehmen", "mobile.calls_host_rec_title": "Sie zeichnest gerade auf", @@ -929,5 +929,7 @@ "mobile.calls_request_message": "Die Anrufe laufen derzeit im Testmodus, und nur Systemadministratoren können sie starten. Wende dich für Hilfe direkt an deinen Systemadministrator", "mobile.calls_participant_limit_title_GA": "Dieser Aufruf ist ausgelastet", "mobile.calls_limit_msg_GA": "Upgrade auf Cloud Professional oder Cloud Enterprise, um Gruppenanrufe mit mehr als {maxParticipants} Teilnehmern zu ermöglichen.", - "general_settings.report_problem": "Fehler melden" + "general_settings.report_problem": "Fehler melden", + "thread.loadingReplies": "Lade Antworten...", + "mobile.calls_open_channel": "Kanal öffnen" } From 96bea4b8263872ff524328b5fa68a863eec64391 Mon Sep 17 00:00:00 2001 From: kaakaa Date: Mon, 19 Dec 2022 04:42:46 +0000 Subject: [PATCH 10/71] Translated using Weblate (Japanese) Currently translated at 100.0% (933 of 933 strings) Translation: mattermost-languages-shipped/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/ja/ --- assets/base/i18n/ja.json | 57 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/assets/base/i18n/ja.json b/assets/base/i18n/ja.json index 45f0a79d6a..2d4f077322 100644 --- a/assets/base/i18n/ja.json +++ b/assets/base/i18n/ja.json @@ -540,7 +540,7 @@ "mobile.custom_status.clear_after.title": "カスタムステータス解除時刻", "mobile.create_post.read_only": "このチャンネルは読み取り専用です。", "mobile.create_direct_message.you": "@{username} - あなた", - "mobile.create_direct_message.start": "開始", + "mobile.create_direct_message.start": "会話を始める", "mobile.create_channel.title": "新しいチャンネル", "mobile.components.select_server_view.msg_welcome": "ようこそ", "mobile.components.select_server_view.msg_description": "サーバーは、あるURLを通じてアクセスできるチームのコミュニケーションHubです", @@ -555,7 +555,7 @@ "mobile.calls_viewing_screen": "{name} の画面を表示しています", "mobile.calls_unmute": "ミュート解除", "mobile.calls_start_call": "通話を開始する", - "mobile.calls_speaker": "発話者", + "mobile.calls_speaker": "スピーカー", "mobile.calls_see_logs": "サーバーログを見る", "mobile.calls_raise_hand": "手を挙げる", "mobile.calls_ok": "OK", @@ -880,5 +880,56 @@ "extension.no_memberships.description": "コンテンツを共有するには、Mattermostサーバー上のメンバになる必要があります。", "connection_banner.not_reachable": "サーバーに到達できません", "connection_banner.not_connected": "インターネットに接続していません", - "connection_banner.connected": "接続が回復しました" + "connection_banner.connected": "接続が回復しました", + "thread.loadingReplies": "返信を読み込んでいます...", + "share_extension.upload_disabled": "選択したサーバーではファイルアップロードが無効化されています", + "share_extension.share_screen.title": "Mattermostに共有する", + "share_extension.servers_screen.title": "サーバーを選択する", + "share_extension.server_label": "サーバー", + "share_extension.multiple_label": "{count, number} 添付ファイル", + "share_extension.message": "メッセージを入力 (任意)", + "share_extension.max_resolution": "画像が最大解像度である7680 x 4320 px を超えています", + "share_extension.file_limit.single": "ファイルは {size} 以下のサイズである必要があります", + "share_extension.file_limit.multiple": "各ファイルは {size} 以下のサイズである必要があります", + "share_extension.count_limit": "このサーバーでは、 {count, number} {count, plural, one {ファイル} other {ファイル}}しか共有できません", + "share_extension.channel_label": "チャンネル", + "share_extension.channel_error": "あなたは選択したサーバーのチームのメンバーではありません。他のサーバーを選択するか、Mattermostを開いてチームに参加してください。", + "settings.advanced.delete_message.confirmation": "\nアプリを通じてサーバーからダウンロードされたファイルが削除されます。確認を行うことで、削除を実行できます。\n", + "settings.advanced.delete_data": "ローカルファイルを削除する", + "settings.advanced.delete": "削除", + "settings.advanced.cancel": "キャンセル", + "onboaring.welcome_description": "Mattermostは開発者のコラボレーションのためのオープンソースのプラットフォームです。セキュアで、柔軟で、さまざまなツールと連携することができます。", + "onboarding.welcome": "ようこそ", + "onboarding.realtime_collaboration_description": "永続的なチャンネル、ダイレクトメッセージ、ファイル共有がシームレスに行われるため、どこにいても常に繋がることができます。", + "onboarding.realtime_collaboration": "リアルタイムにコラボレーション", + "onboarding.integrations_description": "開発プロセスにマッチしたソリューションと統合することでチャットをより使いやすく。", + "onboarding.integrations": "お好みのツールとの連携", + "onboarding.calls_description": "タイピングが追いつかない場合は、1タップでチャンネルベースのチャットからセキュアな音声通話に切り替えることができます。", + "onboarding.calls": "セキュアな音声通話をすぐに開始する", + "notification_settings.mentions.keywords_mention": "メンションのトリガーとなるキーワード", + "notification_settings.auto_responder.message": "メッセージ", + "mobile.open_dm.error": "{displayName} とのダイレクトメッセージを開くことができませんでした。接続を確認し、もう一度試してみてください。", + "mobile.onboarding.sign_in_to_get_started": "サインインして始めましょう", + "mobile.onboarding.sign_in": "サインイン", + "mobile.onboarding.next": "次へ", + "mobile.create_direct_message.max_limit_reached": "グループメッセージは {maxCount} 人までに制限されます", + "mobile.calls_stop_recording": "レコーディング停止", + "mobile.calls_request_title": "現在、Calls機能は有効ではありません", + "mobile.calls_request_message": "現在、Callsはテストモードで稼働しており、システム管理者のみが通話を開始できます。サポートが必要な場合は、システム管理者に問い合わせてください", + "mobile.calls_record": "レコーディング", + "mobile.calls_rec": "レコーディング", + "mobile.calls_react": "リアクション", + "mobile.calls_participant_rec_title": "レコーディング中", + "mobile.calls_participant_rec": "ホストがこの会議のレコーディングを開始しました。ミーティングに参加し続けることで、レコーディングに同意したことになります。", + "mobile.calls_host_rec_stopped": "レコーディングは、処理が完了した後、この通話のチャットスレッドから確認できるようになります。", + "mobile.calls_participant_limit_title_GA": "この通話は定員に達しています", + "mobile.calls_open_channel": "チャンネルを開く", + "mobile.calls_okay": "OK", + "mobile.calls_limit_msg_GA": "{maxParticipants} 人以上でのグループ通話を有効にするには、Cloud ProfessionalかCloud Enterpriseにアップグレードしてください。", + "mobile.calls_host_rec_title": "レコーディングしています", + "mobile.calls_host_rec_stopped_title": "レコーディングが停止しました。処理しています...", + "mobile.calls_host_rec": "あなたはこの会議をレコーディングしています。この会議が記録されていることを全員に知らせることを検討してください。", + "mobile.calls_host": "ホスト", + "mobile.calls_dismiss": "破棄", + "mobile.calls_call_thread": "Callスレッド" } From afc521962963535245197ec2476ef052ab7cab79 Mon Sep 17 00:00:00 2001 From: master7 Date: Mon, 19 Dec 2022 10:00:42 +0000 Subject: [PATCH 11/71] Translated using Weblate (Polish) Currently translated at 100.0% (933 of 933 strings) Translation: mattermost-languages-shipped/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/pl/ --- assets/base/i18n/pl.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/base/i18n/pl.json b/assets/base/i18n/pl.json index f7a318c5ca..74ab0e5422 100644 --- a/assets/base/i18n/pl.json +++ b/assets/base/i18n/pl.json @@ -900,7 +900,7 @@ "mobile.calls_host_rec": "Nagrywasz to spotkanie. Rozważ poinformowanie wszystkich o tym, że to spotkanie jest nagrywane.", "mobile.calls_host": "host", "mobile.calls_dismiss": "Odrzuć", - "mobile.calls_call_thread": "Kanał otwarty", + "mobile.calls_call_thread": "Wątek połączeń", "extension.no_servers.title": "Nie podłączony do żadnych serwerów", "extension.no_servers.description": "Aby udostępniać treści, musisz być zalogowany na serwerze Mattermost.", "extension.no_memberships.title": "Nie jest jeszcze członkiem żadnego zespołu", @@ -929,5 +929,7 @@ "general_settings.report_problem": "Zgłoś problem", "connection_banner.not_reachable": "Serwer jest nieosiągalny", "connection_banner.not_connected": "Brak połączenia z internetem", - "connection_banner.connected": "Połączenie przywrócone" + "connection_banner.connected": "Połączenie przywrócone", + "thread.loadingReplies": "Ładowanie odpowiedzi...", + "mobile.calls_open_channel": "Otwórz kanał" } From f7e7ffdc2cffa1004c4a3383839931bfac26db2f Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 19 Dec 2022 11:53:54 +0000 Subject: [PATCH 12/71] Translated using Weblate (Russian) Currently translated at 100.0% (933 of 933 strings) Translation: mattermost-languages-shipped/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/ru/ --- assets/base/i18n/ru.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/base/i18n/ru.json b/assets/base/i18n/ru.json index ece648e203..335d7a3de9 100644 --- a/assets/base/i18n/ru.json +++ b/assets/base/i18n/ru.json @@ -745,7 +745,7 @@ "mobile.calls_react": "Реакции", "mobile.calls_not_available_option": "(Недоступно)", "mobile.calls_lower_hand": "Опустить руку", - "mobile.calls_call_thread": "Открытый канал", + "mobile.calls_call_thread": "Обсуждение звонка", "channel_modal.nameEx": "Ошибки, Маркетинг", "video.failed_description": "Возникла ошибка при воспроизведении видео.\n", "video.download": "Скачать видео", @@ -929,5 +929,7 @@ "general_settings.report_problem": "Сообщить о проблеме", "connection_banner.not_reachable": "Сервер недоступен", "connection_banner.not_connected": "Нет соединения с интернетом", - "connection_banner.connected": "Соединение восстановлено" + "connection_banner.connected": "Соединение восстановлено", + "thread.loadingReplies": "Загрузка ответов...", + "mobile.calls_open_channel": "Открыть канал" } From 88fde2cc5eab66bf734b33f49ddb28e8cdde2550 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Mon, 19 Dec 2022 21:29:11 +0200 Subject: [PATCH 13/71] Restyle video playback error (#6871) * Restyle video playback error * video not shown when attached with multiple files and thumb fails * update local path when saving and sharing a file * feedback review --- app/components/files/files.tsx | 7 +- app/components/files/video_file.tsx | 12 +- .../document_renderer/document_renderer.tsx | 4 +- .../footer/download_with_action/index.tsx | 5 + app/screens/gallery/video_renderer/error.tsx | 145 ++++++++++++++++++ app/screens/gallery/video_renderer/index.tsx | 40 +++-- assets/base/i18n/en.json | 3 +- 7 files changed, 188 insertions(+), 28 deletions(-) create mode 100644 app/screens/gallery/video_renderer/error.tsx diff --git a/app/components/files/files.tsx b/app/components/files/files.tsx index 5f45f878ec..b4a404b92f 100644 --- a/app/components/files/files.tsx +++ b/app/components/files/files.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {DeviceEventEmitter, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'; import Animated, {useDerivedValue} from 'react-native-reanimated'; @@ -69,10 +69,9 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l filesForGallery.value[idx] = file; }; - const isSingleImage = () => (filesInfo.length === 1 && (isImage(filesInfo[0]) || isVideo(filesInfo[0]))); + const isSingleImage = useMemo(() => filesInfo.filter((f) => isImage(f) || isVideo(f)).length === 1, [filesInfo]); const renderItems = (items: FileInfo[], moreImagesCount = 0, includeGutter = false) => { - const singleImage = isSingleImage(); let nonVisibleImagesCount: number; let container: StyleProp = items.length > 1 ? styles.container : undefined; const containerWithGutter = [container, styles.gutter]; @@ -97,7 +96,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l file={file} index={attachmentIndex(file.id!)} onPress={handlePreviewPress} - isSingleImage={singleImage} + isSingleImage={isSingleImage} nonVisibleImagesCount={nonVisibleImagesCount} publicLinkEnabled={publicLinkEnabled} updateFileForGallery={updateFileForGallery} diff --git a/app/components/files/video_file.tsx b/app/components/files/video_file.tsx index 7b7ef0f863..c5b7d919ec 100644 --- a/app/components/files/video_file.tsx +++ b/app/components/files/video_file.tsx @@ -76,9 +76,8 @@ const VideoFile = ({ const viewPortHeight = Math.max(dimensions.height, dimensions.width) * 0.45; return calculateDimensions(video.height || wrapperWidth, video.width || wrapperWidth, wrapperWidth, viewPortHeight); } - return undefined; - }, [dimensions.height, dimensions.width, video.height, video.width, wrapperWidth]); + }, [dimensions.height, dimensions.width, video.height, video.width, wrapperWidth, isSingleImage]); const getThumbnail = async () => { const data = {...file}; @@ -161,7 +160,12 @@ const VideoFile = ({ if (failed) { thumbnail = ( - + - {!isSingleImage && } + {!isSingleImage && !failed && } {thumbnail} diff --git a/app/screens/gallery/document_renderer/document_renderer.tsx b/app/screens/gallery/document_renderer/document_renderer.tsx index 693be2b002..cd94fd7d50 100644 --- a/app/screens/gallery/document_renderer/document_renderer.tsx +++ b/app/screens/gallery/document_renderer/document_renderer.tsx @@ -105,8 +105,8 @@ const DocumentRenderer = ({canDownloadFiles, item, onShouldHideControls}: Props) onPress={handleOpenFile} rippleColor={changeOpacity('#fff', 0.16)} > - - {optionText} + + {optionText} diff --git a/app/screens/gallery/footer/download_with_action/index.tsx b/app/screens/gallery/footer/download_with_action/index.tsx index 730516c6a8..ce8b4c5d00 100644 --- a/app/screens/gallery/footer/download_with_action/index.tsx +++ b/app/screens/gallery/footer/download_with_action/index.tsx @@ -13,6 +13,7 @@ import {useAnimatedStyle, withTiming} from 'react-native-reanimated'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import Share from 'react-native-share'; +import {updateLocalFilePath} from '@actions/local/file'; import {downloadFile} from '@actions/remote/file'; import CompassIcon from '@components/compass_icon'; import ProgressBar from '@components/progress_bar'; @@ -173,6 +174,8 @@ const DownloadWithAction = ({action, item, onDownloadSuccess, setAction, gallery return; } + updateLocalFilePath(serverUrl, item.id, path); + Share.open({ url: path, saveToFiles: true, @@ -193,6 +196,7 @@ const DownloadWithAction = ({action, item, onDownloadSuccess, setAction, gallery album: applicationName, }); setSaved(true); + updateLocalFilePath(serverUrl, item.id, path); } catch { setError(intl.formatMessage({id: 'gallery.save_failed', defaultMessage: 'Unable to save the file'})); } @@ -223,6 +227,7 @@ const DownloadWithAction = ({action, item, onDownloadSuccess, setAction, gallery if (response.data?.path) { const path = response.data.path as string; onDownloadSuccess?.(path); + updateLocalFilePath(serverUrl, item.id, path); Share.open({ message: '', title: '', diff --git a/app/screens/gallery/video_renderer/error.tsx b/app/screens/gallery/video_renderer/error.tsx new file mode 100644 index 0000000000..6d88f8fa87 --- /dev/null +++ b/app/screens/gallery/video_renderer/error.tsx @@ -0,0 +1,145 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {Dispatch, SetStateAction, useCallback, useState} from 'react'; +import {StyleSheet, Text, useWindowDimensions, View} from 'react-native'; +import FastImage from 'react-native-fast-image'; +import {RectButton, TouchableWithoutFeedback} from 'react-native-gesture-handler'; +import Animated from 'react-native-reanimated'; + +import {typography} from '@app/utils/typography'; +import CompassIcon from '@components/compass_icon'; +import FormattedText from '@components/formatted_text'; +import {Preferences} from '@constants'; +import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; +import {calculateDimensions} from '@utils/images'; +import {changeOpacity} from '@utils/theme'; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + flex: 1, + maxWidth: 600, + }, + filename: { + color: '#FFF', + ...typography('Body', 200, 'SemiBold'), + marginVertical: 8, + paddingHorizontal: 25, + textAlign: 'center', + }, + unsupported: { + color: '#FFF', + ...typography('Body', 100, 'SemiBold'), + marginTop: 10, + paddingHorizontal: 25, + opacity: 0.64, + textAlign: 'center', + }, + marginBottom: { + marginBottom: 16, + }, + marginTop: { + marginTop: 16, + }, +}); + +type Props = { + filename: string; + height: number; + isDownloading: boolean; + isRemote: boolean; + onShouldHideControls: () => void; + posterUri?: string; + setDownloading: Dispatch>; + width: number; +} + +const VideoError = ({filename, height, isDownloading, isRemote, onShouldHideControls, posterUri, setDownloading, width}: Props) => { + const [hasPoster, setHasPoster] = useState(false); + const [loadPosterError, setLoadPosterError] = useState(false); + + const handleDownload = useCallback(() => { + setDownloading(true); + }, []); + + const handlePosterSet = useCallback(() => { + setHasPoster(true); + }, []); + + const handlePosterError = useCallback(() => { + setLoadPosterError(true); + }, []); + + const dimensions = useWindowDimensions(); + const imageDimensions = calculateDimensions(height, width, dimensions.width); + + let poster; + if (posterUri && !loadPosterError) { + poster = ( + + ); + } else { + poster = ( + + ); + } + + return ( + + + {poster} + + {filename} + + {isRemote && + + + + + + + + + + + } + {!isRemote && + + } + + + ); +}; + +export default VideoError; diff --git a/app/screens/gallery/video_renderer/index.tsx b/app/screens/gallery/video_renderer/index.tsx index 6df79d8992..0fb34f66a2 100644 --- a/app/screens/gallery/video_renderer/index.tsx +++ b/app/screens/gallery/video_renderer/index.tsx @@ -2,8 +2,7 @@ // See LICENSE.txt for license information. import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {useIntl} from 'react-intl'; -import {Alert, DeviceEventEmitter, Platform, StyleSheet, useWindowDimensions} from 'react-native'; +import {DeviceEventEmitter, Platform, StyleSheet, useWindowDimensions} from 'react-native'; import Animated, {Easing, useAnimatedRef, useAnimatedStyle, useSharedValue, withTiming, WithTimingConfig} from 'react-native-reanimated'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import Video, {OnPlaybackRateData} from 'react-native-video'; @@ -17,6 +16,8 @@ import {changeOpacity} from '@utils/theme'; import DownloadWithAction from '../footer/download_with_action'; +import VideoError from './error'; + import type {ImageRendererProps} from '../image_renderer'; import type {GalleryAction} from '@typings/screens/gallery'; @@ -55,7 +56,6 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul const dimensions = useWindowDimensions(); const fullscreen = useSharedValue(false); const {bottom} = useSafeAreaInsets(); - const {formatMessage} = useIntl(); const serverUrl = useServerUrl(); const videoRef = useAnimatedRef diff --git a/app/components/post_draft/send_handler/send_handler.tsx b/app/components/post_draft/send_handler/send_handler.tsx index 6ffb5fb51d..97c7e7e696 100644 --- a/app/components/post_draft/send_handler/send_handler.tsx +++ b/app/components/post_draft/send_handler/send_handler.tsx @@ -13,6 +13,7 @@ import {setStatus} from '@actions/remote/user'; import {canEndCall, endCall, getEndCallMessage} from '@calls/actions/calls'; import ClientError from '@client/rest/error'; import {Events, Screens} from '@constants'; +import {PostPriorityType} from '@constants/post'; import {NOTIFY_ALL_MEMBERS} from '@constants/post_draft'; import {useServerUrl} from '@context/server'; import DraftUploadManager from '@managers/draft_upload_manager'; @@ -54,6 +55,10 @@ type Props = { uploadFileError: React.ReactNode; } +const INITIAL_PRIORITY = { + priority: PostPriorityType.STANDARD, +}; + export default function SendHandler({ testID, channelId, @@ -83,8 +88,7 @@ export default function SendHandler({ const [channelTimezoneCount, setChannelTimezoneCount] = useState(0); const [sendingMessage, setSendingMessage] = useState(false); - - const [postProps, setPostProps] = useState({}); + const [postPriority, setPostPriority] = useState(INITIAL_PRIORITY); const canSend = useCallback(() => { if (sendingMessage) { @@ -120,17 +124,19 @@ export default function SendHandler({ message: value, } as Post; - if (Object.keys(postProps).length) { - post.props = postProps; + if (Object.keys(postPriority).length) { + post.metadata = { + priority: postPriority, + }; } createPost(serverUrl, post, postFiles); clearDraft(); setSendingMessage(false); - setPostProps({}); + setPostPriority(INITIAL_PRIORITY); DeviceEventEmitter.emit(Events.POST_LIST_SCROLL_TO_BOTTOM, rootId ? Screens.THREAD : Screens.CHANNEL); - }, [files, currentUserId, channelId, rootId, value, clearDraft, postProps]); + }, [files, currentUserId, channelId, rootId, value, clearDraft, postPriority]); const showSendToAllOrChannelOrHereAlert = useCallback((calculatedMembersCount: number, atHere: boolean) => { const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(intl, calculatedMembersCount, Boolean(isTimezoneEnabled), channelTimezoneCount, atHere); @@ -297,8 +303,8 @@ export default function SendHandler({ canSend={canSend()} maxMessageLength={maxMessageLength} updatePostInputTop={updatePostInputTop} - postProps={postProps} - updatePostProps={setPostProps} + postPriority={postPriority} + updatePostPriority={setPostPriority} setIsFocused={setIsFocused} /> ); diff --git a/app/components/post_list/post/header/header.tsx b/app/components/post_list/post/header/header.tsx index fafe6035bc..7b9eaab895 100644 --- a/app/components/post_list/post/header/header.tsx +++ b/app/components/post_list/post/header/header.tsx @@ -132,10 +132,10 @@ const Header = (props: HeaderProps) => { style={style.time} testID='post_header.date_time' /> - {showPostPriority && ( + {showPostPriority && post.metadata?.priority?.priority && ( )} diff --git a/app/components/post_list/post/post.tsx b/app/components/post_list/post/post.tsx index a1e6f1107a..a177816680 100644 --- a/app/components/post_list/post/post.tsx +++ b/app/components/post_list/post/post.tsx @@ -226,7 +226,7 @@ const Post = ({ // If the post is a priority post: // 1. Show the priority label in channel screen // 2. Show the priority label in thread screen for the root post - const showPostPriority = Boolean(isPostPriorityEnabled && post.props?.priority) && (location !== Screens.THREAD || !post.rootId); + const showPostPriority = Boolean(isPostPriorityEnabled && post.metadata?.priority?.priority) && (location !== Screens.THREAD || !post.rootId); const sameSequence = hasReplies ? (hasReplies && post.rootId) : !post.rootId; if (!showPostPriority && hasSameRoot && isConsecutivePost && sameSequence) { diff --git a/app/components/post_priority/post_priority_label.tsx b/app/components/post_priority/post_priority_label.tsx index 3432b65540..e9aeedd1d5 100644 --- a/app/components/post_priority/post_priority_label.tsx +++ b/app/components/post_priority/post_priority_label.tsx @@ -35,7 +35,7 @@ const style = StyleSheet.create({ }); type Props = { - label: PostPriorityType; + label: PostPriorityData['priority']; }; const PostPriorityLabel = ({label}: Props) => { @@ -48,7 +48,7 @@ const PostPriorityLabel = ({label}: Props) => { containerStyle.push(style.urgent); iconName = 'alert-outline'; labelText = intl.formatMessage({id: 'post_priority.label.urgent', defaultMessage: 'URGENT'}); - } else { + } else if (label === PostPriorityType.IMPORTANT) { containerStyle.push(style.important); iconName = 'alert-circle-outline'; labelText = intl.formatMessage({id: 'post_priority.label.important', defaultMessage: 'IMPORTANT'}); diff --git a/app/components/post_priority/post_priority_picker/index.tsx b/app/components/post_priority/post_priority_picker/index.tsx index 9de0b58d0c..48c51e60a1 100644 --- a/app/components/post_priority/post_priority_picker/index.tsx +++ b/app/components/post_priority/post_priority_picker/index.tsx @@ -14,10 +14,6 @@ import {typography} from '@utils/typography'; import PostPriorityPickerItem from './post_priority_picker_item'; -export type PostPriorityData = { - priority: PostPriorityType; -}; - type Props = { data: PostPriorityData; onSubmit: (data: PostPriorityData) => void; @@ -61,8 +57,8 @@ const PostPriorityPicker = ({data, onSubmit}: Props) => { // For now, we just have one option but the spec suggest we have more in the next phase // const [data, setData] = React.useState(defaultData); - const handleUpdatePriority = React.useCallback((priority: PostPriorityType) => { - onSubmit({priority}); + const handleUpdatePriority = React.useCallback((priority: PostPriorityData['priority']) => { + onSubmit({priority: priority || ''}); }, [onSubmit]); return ( diff --git a/types/api/posts.d.ts b/types/api/posts.d.ts index c36824e4f5..51da8a0bfb 100644 --- a/types/api/posts.d.ts +++ b/types/api/posts.d.ts @@ -20,6 +20,10 @@ type PostType = type PostEmbedType = 'image' | 'message_attachment' | 'opengraph'; +type PostPriorityData = { + priority: ''|'urgent'|'important'; +}; + type PostEmbed = { type: PostEmbedType; url: string; @@ -39,6 +43,7 @@ type PostMetadata = { files?: FileInfo[]; images?: Dictionary; reactions?: Reaction[]; + priority?: PostPriorityData; }; type Post = { From 7663276710ae81e76fb7e73f236d17660602b06e Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 20 Dec 2022 22:33:12 +0200 Subject: [PATCH 22/71] Trigger tab bar animation earlier (#6889) --- app/screens/navigation.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index e8b1882097..8c77176e1b 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -5,7 +5,7 @@ import merge from 'deepmerge'; import {Appearance, DeviceEventEmitter, NativeModules, StatusBar, Platform, Alert} from 'react-native'; -import {ImageResource, Navigation, Options, OptionsModalPresentationStyle, OptionsTopBarButton, ScreenPoppedEvent} from 'react-native-navigation'; +import {ComponentWillAppearEvent, ImageResource, Navigation, Options, OptionsModalPresentationStyle, OptionsTopBarButton, ScreenPoppedEvent} from 'react-native-navigation'; import tinyColor from 'tinycolor2'; import CompassIcon from '@components/compass_icon'; @@ -32,6 +32,7 @@ const alpha = { export function registerNavigationListeners() { Navigation.events().registerScreenPoppedListener(onPoppedListener); Navigation.events().registerCommandListener(onCommandListener); + Navigation.events().registerComponentWillAppearListener(onScreenWillAppear); } function onCommandListener(name: string, params: any) { @@ -66,7 +67,10 @@ function onCommandListener(name: string, params: any) { function onPoppedListener({componentId}: ScreenPoppedEvent) { // screen pop does not trigger registerCommandListener, but does trigger screenPoppedListener NavigationStore.removeScreenFromStack(componentId); - if (NavigationStore.getVisibleScreen() === Screens.HOME) { +} + +function onScreenWillAppear(event: ComponentWillAppearEvent) { + if (event.componentId === Screens.HOME) { DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true); } } From 25291b04f1fa2c0683c4b2bc1afe76a6ba6b588d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 20 Dec 2022 21:35:12 +0100 Subject: [PATCH 23/71] Fix server unreachable message showing too often (#6890) --- app/actions/websocket/index.ts | 6 ++--- .../connection_banner/connection_banner.tsx | 12 +++++++--- app/components/connection_banner/index.ts | 2 +- app/managers/websocket_manager.ts | 23 ++++++++++--------- .../server_item/websocket/index.ts | 2 +- .../server_item/websocket/websocket.tsx | 6 ++--- types/global/websocket.d.ts | 4 ++++ 7 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 types/global/websocket.d.ts diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 5f094dbd37..f2b12615b3 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -86,7 +86,7 @@ export async function handleFirstConnect(serverUrl: string) { // ESR: 5.37 if (lastDisconnect && config?.EnableReliableWebSockets !== 'true' && alreadyConnected.has(serverUrl)) { - handleReconnect(serverUrl); + await handleReconnect(serverUrl); return; } @@ -100,8 +100,8 @@ export async function handleFirstConnect(serverUrl: string) { } } -export function handleReconnect(serverUrl: string) { - doReconnect(serverUrl); +export async function handleReconnect(serverUrl: string) { + await doReconnect(serverUrl); } export async function handleClose(serverUrl: string, lastDisconnect: number) { diff --git a/app/components/connection_banner/connection_banner.tsx b/app/components/connection_banner/connection_banner.tsx index ea1340a720..44604ff32d 100644 --- a/app/components/connection_banner/connection_banner.tsx +++ b/app/components/connection_banner/connection_banner.tsx @@ -20,7 +20,7 @@ import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; type Props = { - isConnected: boolean; + websocketState: WebsocketConnectedState; } const getStyle = makeStyleSheetFromTheme((theme: Theme) => { @@ -74,7 +74,7 @@ const TIME_TO_OPEN = toMilliseconds({seconds: 3}); const TIME_TO_CLOSE = toMilliseconds({seconds: 1}); const ConnectionBanner = ({ - isConnected, + websocketState, }: Props) => { const intl = useIntl(); const closeTimeout = useRef(); @@ -86,6 +86,8 @@ const ConnectionBanner = ({ const appState = useAppState(); const netInfo = useNetInfo(); + const isConnected = websocketState === 'connected'; + const openCallback = useCallback(() => { setVisible(true); clearTimeoutRef(openTimeout); @@ -97,7 +99,9 @@ const ConnectionBanner = ({ }, []); useEffect(() => { - if (!isConnected) { + if (websocketState === 'connecting') { + openCallback(); + } else if (!isConnected) { openTimeout.current = setTimeout(openCallback, TIME_TO_OPEN); } return () => { @@ -158,6 +162,8 @@ const ConnectionBanner = ({ let text; if (isConnected) { text = intl.formatMessage({id: 'connection_banner.connected', defaultMessage: 'Connection restored'}); + } else if (websocketState === 'connecting') { + text = intl.formatMessage({id: 'connection_banner.connecting', defaultMessage: 'Connecting...'}); } else if (netInfo.isInternetReachable) { text = intl.formatMessage({id: 'connection_banner.not_reachable', defaultMessage: 'The server is not reachable'}); } else { diff --git a/app/components/connection_banner/index.ts b/app/components/connection_banner/index.ts index 0b5547837b..5de70a4285 100644 --- a/app/components/connection_banner/index.ts +++ b/app/components/connection_banner/index.ts @@ -9,7 +9,7 @@ import websocket_manager from '@managers/websocket_manager'; import ConnectionBanner from './connection_banner'; const enhanced = withObservables(['serverUrl'], ({serverUrl}: {serverUrl: string}) => ({ - isConnected: websocket_manager.observeConnected(serverUrl), + websocketState: websocket_manager.observeWebsocketState(serverUrl), })); export default withServerUrl(enhanced(ConnectionBanner)); diff --git a/app/managers/websocket_manager.ts b/app/managers/websocket_manager.ts index 809ad34444..1e44a3da2c 100644 --- a/app/managers/websocket_manager.ts +++ b/app/managers/websocket_manager.ts @@ -21,10 +21,10 @@ import {isMainActivity} from '@utils/helpers'; import {logError} from '@utils/log'; const WAIT_TO_CLOSE = toMilliseconds({seconds: 15}); -const WAIT_UNTIL_NEXT = toMilliseconds({seconds: 20}); +const WAIT_UNTIL_NEXT = toMilliseconds({seconds: 5}); class WebsocketManager { - private connectedSubjects: {[serverUrl: string]: BehaviorSubject} = {}; + private connectedSubjects: {[serverUrl: string]: BehaviorSubject} = {}; private clients: Record = {}; private connectionTimerIDs: Record void>> = {}; @@ -69,7 +69,7 @@ class WebsocketManager { } delete this.clients[serverUrl]; - this.getConnectedSubject(serverUrl).next(false); + this.getConnectedSubject(serverUrl).next('not_connected'); delete this.connectedSubjects[serverUrl]; }; @@ -96,7 +96,7 @@ class WebsocketManager { const client = this.clients[url]; if (client.isConnected()) { client.close(true); - this.getConnectedSubject(url).next(false); + this.getConnectedSubject(url).next('not_connected'); } } }; @@ -107,6 +107,7 @@ class WebsocketManager { if (clientUrl === activeServerUrl) { this.initializeClient(clientUrl); } else { + this.getConnectedSubject(clientUrl).next('connecting'); const bounce = debounce(this.initializeClient.bind(this, clientUrl), WAIT_UNTIL_NEXT); this.connectionTimerIDs[clientUrl] = bounce; bounce(); @@ -118,7 +119,7 @@ class WebsocketManager { return this.clients[serverUrl]?.isConnected(); }; - public observeConnected = (serverUrl: string) => { + public observeWebsocketState = (serverUrl: string) => { return this.getConnectedSubject(serverUrl).asObservable().pipe( distinctUntilChanged(), ); @@ -126,7 +127,7 @@ class WebsocketManager { private getConnectedSubject = (serverUrl: string) => { if (!this.connectedSubjects[serverUrl]) { - this.connectedSubjects[serverUrl] = new BehaviorSubject(this.isConnected(serverUrl)); + this.connectedSubjects[serverUrl] = new BehaviorSubject(this.isConnected(serverUrl) ? 'connected' : 'not_connected'); } return this.connectedSubjects[serverUrl]; @@ -153,13 +154,13 @@ class WebsocketManager { private onFirstConnect = (serverUrl: string) => { this.startPeriodicStatusUpdates(serverUrl); handleFirstConnect(serverUrl); - this.getConnectedSubject(serverUrl).next(true); + this.getConnectedSubject(serverUrl).next('connected'); }; - private onReconnect = (serverUrl: string) => { + private onReconnect = async (serverUrl: string) => { this.startPeriodicStatusUpdates(serverUrl); - handleReconnect(serverUrl); - this.getConnectedSubject(serverUrl).next(true); + await handleReconnect(serverUrl); + this.getConnectedSubject(serverUrl).next('connected'); }; private onWebsocketClose = async (serverUrl: string, connectFailCount: number, lastDisconnect: number) => { @@ -168,7 +169,7 @@ class WebsocketManager { await handleClose(serverUrl, lastDisconnect); this.stopPeriodicStatusUpdates(serverUrl); - this.getConnectedSubject(serverUrl).next(false); + this.getConnectedSubject(serverUrl).next('not_connected'); } }; diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/websocket/index.ts b/app/screens/home/channel_list/servers/servers_list/server_item/websocket/index.ts index de3f977534..b3379ffe77 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/websocket/index.ts +++ b/app/screens/home/channel_list/servers/servers_list/server_item/websocket/index.ts @@ -8,7 +8,7 @@ import WebsocketManager from '@managers/websocket_manager'; import WebSocket from './websocket'; const enhanced = withObservables(['serverUrl'], ({serverUrl}: {serverUrl: string}) => ({ - isConnected: WebsocketManager.observeConnected(serverUrl), + websocketState: WebsocketManager.observeWebsocketState(serverUrl), })); export default enhanced(WebSocket); diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/websocket/websocket.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/websocket/websocket.tsx index 19cac50798..f4c162a23e 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/websocket/websocket.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/websocket/websocket.tsx @@ -11,7 +11,7 @@ import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; type Props = { - isConnected: boolean; + websocketState: WebsocketConnectedState; } const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ @@ -28,10 +28,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ }, })); -const WebSocket = ({isConnected}: Props) => { +const WebSocket = ({websocketState}: Props) => { const theme = useTheme(); - if (isConnected) { + if (websocketState === 'connected' || websocketState === 'connecting') { return null; } diff --git a/types/global/websocket.d.ts b/types/global/websocket.d.ts new file mode 100644 index 0000000000..9ee8f65e5e --- /dev/null +++ b/types/global/websocket.d.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +type WebsocketConnectedState = 'not_connected' | 'connected' | 'connecting'; From 51f07df074e1d31e141577856bad83b8e4228386 Mon Sep 17 00:00:00 2001 From: Joseph Baylon Date: Tue, 20 Dec 2022 13:53:45 -0800 Subject: [PATCH 24/71] Stabilize smoke test threads e2e --- detox/e2e/test/smoke_test/threads.e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/detox/e2e/test/smoke_test/threads.e2e.ts b/detox/e2e/test/smoke_test/threads.e2e.ts index 864446a9f6..25b2875a61 100644 --- a/detox/e2e/test/smoke_test/threads.e2e.ts +++ b/detox/e2e/test/smoke_test/threads.e2e.ts @@ -133,6 +133,7 @@ describe('Smoke Test - Threads', () => { // # Go back to global threads screen, open thread options for thread, tap on save option, and tap on thread await ThreadScreen.back(); await GlobalThreadsScreen.openThreadOptionsFor(parentPost.id); + await wait(timeouts.ONE_SEC); await ThreadOptionsScreen.unsaveThreadOption.tap(); await wait(timeouts.ONE_SEC); await GlobalThreadsScreen.getThreadItem(parentPost.id).tap(); From 58b78ec27a02637a3983253d62ff86b0c60bd85f Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 21 Dec 2022 16:00:38 +0200 Subject: [PATCH 25/71] use correct syntax highlight styles for github code theme (#6895) --- app/components/syntax_highlight/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/syntax_highlight/index.tsx b/app/components/syntax_highlight/index.tsx index a83b2e3e01..8ddcdd63b0 100644 --- a/app/components/syntax_highlight/index.tsx +++ b/app/components/syntax_highlight/index.tsx @@ -4,7 +4,7 @@ import React, {useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import SyntaxHighlighter from 'react-syntax-highlighter'; -import {github, monokai, solarizedDark, solarizedLight} from 'react-syntax-highlighter/dist/cjs/styles/hljs'; +import {githubGist as github, monokai, solarizedDark, solarizedLight} from 'react-syntax-highlighter/dist/cjs/styles/hljs'; import {useTheme} from '@context/theme'; From 92ca1e3704df8778ae59ca205e058217437045e3 Mon Sep 17 00:00:00 2001 From: Avinash Lingaloo Date: Wed, 21 Dec 2022 21:34:44 +0400 Subject: [PATCH 26/71] Sentry fixes (#6830) --- app/init/push_notifications.ts | 4 +- app/screens/sso/sso_with_redirect_url.tsx | 4 +- app/utils/error_handling.ts | 6 ++- app/utils/general/index.ts | 3 ++ app/utils/sentry.ts | 23 ++++++++-- fastlane/Fastfile | 6 ++- ios/ErrorReporting/Sentry.swift | 27 +++++++++++ ios/Mattermost.xcodeproj/project.pbxproj | 45 +++++++++++++++++-- .../xcshareddata/swiftpm/Package.resolved | 9 ++++ ios/MattermostShare/Info.plist | 8 +++- ios/MattermostShare/ShareViewController.swift | 7 ++- ios/NotificationService/Info.plist | 4 ++ .../NotificationService.swift | 6 ++- 13 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 ios/ErrorReporting/Sentry.swift diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index ec78be8f7e..14aac23155 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import {AppState, DeviceEventEmitter, Platform} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; import { Notification, NotificationAction, @@ -29,6 +28,7 @@ import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread'; import {dismissOverlay, showOverlay} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; +import {isBetaApp} from '@utils/general'; import {isMainActivity, isTablet} from '@utils/helpers'; import {logInfo} from '@utils/log'; import {convertToNotificationData} from '@utils/notification'; @@ -248,7 +248,7 @@ class PushNotifications { if (Platform.OS === 'ios') { prefix = Device.PUSH_NOTIFY_APPLE_REACT_NATIVE; - if (DeviceInfo.getBundleId().includes('rnbeta')) { + if (isBetaApp) { prefix = `${prefix}beta`; } } else { diff --git a/app/screens/sso/sso_with_redirect_url.tsx b/app/screens/sso/sso_with_redirect_url.tsx index 30867c24de..5eb39f7a62 100644 --- a/app/screens/sso/sso_with_redirect_url.tsx +++ b/app/screens/sso/sso_with_redirect_url.tsx @@ -6,13 +6,13 @@ import React, {useEffect, useState} from 'react'; import {useIntl} from 'react-intl'; import {Linking, Platform, Text, View} from 'react-native'; import Button from 'react-native-button'; -import DeviceInfo from 'react-native-device-info'; import urlParse from 'url-parse'; import FormattedText from '@components/formatted_text'; import {Sso} from '@constants'; import NetworkManager from '@managers/network_manager'; import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; +import {isBetaApp} from '@utils/general'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {tryOpenURL} from '@utils/url'; @@ -62,7 +62,7 @@ const SSOWithRedirectURL = ({doSSOLogin, loginError, loginUrl, serverUrl, setLog const style = getStyleSheet(theme); const intl = useIntl(); let customUrlScheme = Sso.REDIRECT_URL_SCHEME; - if (DeviceInfo.getBundleId && DeviceInfo.getBundleId().includes('rnbeta')) { + if (isBetaApp) { customUrlScheme = Sso.REDIRECT_URL_SCHEME_DEV; } diff --git a/app/utils/error_handling.ts b/app/utils/error_handling.ts index 721a05b68b..2ac1145fda 100644 --- a/app/utils/error_handling.ts +++ b/app/utils/error_handling.ts @@ -11,6 +11,7 @@ import { import {DEFAULT_LOCALE, getTranslations, t} from '@i18n'; import {dismissAllModals} from '@screens/navigation'; import {ClientError} from '@utils/client_error'; +import {isBetaApp} from '@utils/general'; import { captureException, captureJSException, @@ -42,7 +43,10 @@ class JavascriptAndNativeErrorHandler { } logWarning('Handling Javascript error', e, isFatal); - captureJSException(e, isFatal); + + if (isBetaApp || isFatal) { + captureJSException(e, isFatal); + } if (isFatal && e instanceof Error) { const translations = getTranslations(DEFAULT_LOCALE); diff --git a/app/utils/general/index.ts b/app/utils/general/index.ts index 60bc8d2793..1a29fa1ba1 100644 --- a/app/utils/general/index.ts +++ b/app/utils/general/index.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import DeviceInfo from 'react-native-device-info'; import ReactNativeHapticFeedback, {HapticFeedbackTypes} from 'react-native-haptic-feedback'; type SortByCreatAt = (Session | Channel | Team | Post) & { @@ -51,3 +52,5 @@ export const sortByNewest = (a: SortByCreatAt, b: SortByCreatAt) => { return 1; }; + +export const isBetaApp = DeviceInfo.getBundleId && DeviceInfo.getBundleId().includes('rnbeta'); diff --git a/app/utils/sentry.ts b/app/utils/sentry.ts index cf071c10b3..44b5e7bb99 100644 --- a/app/utils/sentry.ts +++ b/app/utils/sentry.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {Database} from '@nozbe/watermelondb'; -import {Breadcrumb} from '@sentry/types'; +import {Breadcrumb, Event} from '@sentry/types'; import {Platform} from 'react-native'; import {Navigation} from 'react-native-navigation'; @@ -10,6 +10,7 @@ import Config from '@assets/config.json'; import DatabaseManager from '@database/manager'; import {getConfig} from '@queries/servers/system'; import {getCurrentUser} from '@queries/servers/user'; +import {isBetaApp} from '@utils/general'; import {ClientError} from './client_error'; import {logError, logWarning} from './log'; @@ -39,9 +40,18 @@ export function initializeSentry() { return; } + const mmConfig = { + environment: isBetaApp ? 'beta' : 'production', + tracesSampleRate: isBetaApp ? 1.0 : 0.2, + sampleRate: isBetaApp ? 1.0 : 0.2, + attachStacktrace: isBetaApp, // For Beta, stack traces are automatically attached to all messages logged + }; + Sentry.init({ dsn, - tracesSampleRate: 0.2, + sendDefaultPii: false, + ...mmConfig, + ...Config.SentryOptions, integrations: [ new Sentry.ReactNativeTracing({ @@ -51,8 +61,13 @@ export function initializeSentry() { ), }), ], - sendDefaultPii: false, - ...Config.SentryOptions, + beforeSend: (event: Event) => { + if (isBetaApp || event?.level === 'fatal') { + return event; + } + + return null; + }, }); } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3fad4a6b8d..7e281fb267 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -568,6 +568,9 @@ platform :ios do app_name_sub = app_name.gsub(" ", "_") config_mode = ENV['BUILD_FOR_RELEASE'] == 'true' ? 'Release' : 'Debug' method = ENV['IOS_BUILD_EXPORT_METHOD'].nil? || ENV['IOS_BUILD_EXPORT_METHOD'].empty? ? 'ad-hoc' : ENV['IOS_BUILD_EXPORT_METHOD'] + + # Need to add xcargs to only notification and + xcargs = ENV['SENTRY_ENABLED'] == 'true' ? "SENTRY_DSN_IOS='#{ENV['SENTRY_DSN_IOS']}' SENTRY_ENABLED='#{ENV['SENTRY_ENABLED']}'" : '' setup_code_signing @@ -583,7 +586,8 @@ platform :ios do export_options: { signingStyle: 'manual', iCloudContainerEnvironment: 'Production' - } + }, + xcargs:xcargs ) end diff --git a/ios/ErrorReporting/Sentry.swift b/ios/ErrorReporting/Sentry.swift new file mode 100644 index 0000000000..71b8def203 --- /dev/null +++ b/ios/ErrorReporting/Sentry.swift @@ -0,0 +1,27 @@ +// +// Sentry.swift +// Mattermost +// +// Created by Avinash Lingaloo on 20/12/2022. +// Copyright © 2022 Facebook. All rights reserved. +// +import Foundation + +import Sentry + +func initSentryAppExt(){ + if let SENTRY_ENABLED = Bundle.main.infoDictionary?["SENTRY_ENABLED"] as? String, + let SENTRY_DSN = Bundle.main.infoDictionary?["SENTRY_DSN_IOS"] as? String { + if(SENTRY_ENABLED=="true"){ + SentrySDK.start { options in + options.dsn = SENTRY_DSN + options.enableAppHangTracking = true + options.enableCaptureFailedRequests = true + } + } + } +} + +func testSentry(msg: String){ + SentrySDK.capture(message: msg) +} diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 02f0ef2c02..11feef5184 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -11,6 +11,10 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 27C667A329523ECA00E590D5 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 27C667A229523ECA00E590D5 /* Sentry */; }; + 27C667A529523F0A00E590D5 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 27C667A429523F0A00E590D5 /* Sentry */; }; + 27C667A9295241B600E590D5 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C667A8295241B600E590D5 /* Sentry.swift */; }; + 27C667AA295241B600E590D5 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C667A8295241B600E590D5 /* Sentry.swift */; }; 49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370026D4455D00EF4E52 /* Gekidou */; }; 536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */; }; 58495E36BF1A4EAB93609E57 /* Metropolis-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 54956DEFEBB74EF78C3A6AE5 /* Metropolis-SemiBold.ttf */; }; @@ -156,6 +160,7 @@ 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Mattermost/main.m; sourceTree = ""; }; 182D203F539AF68F1647EFAF /* Pods-Mattermost-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-Mattermost-MattermostTests/Pods-Mattermost-MattermostTests.release.xcconfig"; sourceTree = ""; }; 25BF2BACE89201DE6E585B7E /* Pods-Mattermost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.release.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.release.xcconfig"; sourceTree = ""; }; + 27C667A8295241B600E590D5 /* Sentry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentry.swift; sourceTree = ""; }; 297AAFCCF0BD99FC109DA2BC /* Pods-MattermostTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MattermostTests.release.xcconfig"; path = "Target Support Files/Pods-MattermostTests/Pods-MattermostTests.release.xcconfig"; sourceTree = ""; }; 32AC3D4EA79E44738A6E9766 /* OpenSans-BoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-BoldItalic.ttf"; path = "../assets/fonts/OpenSans-BoldItalic.ttf"; sourceTree = ""; }; 3647DF63D6764CF093375861 /* OpenSans-ExtraBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBold.ttf"; path = "../assets/fonts/OpenSans-ExtraBold.ttf"; sourceTree = ""; }; @@ -279,6 +284,7 @@ buildActionMask = 2147483647; files = ( 49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */, + 27C667A329523ECA00E590D5 /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -288,6 +294,7 @@ files = ( 7FD4822C2864D73300A5B18B /* OpenGraph in Frameworks */, 7F4288042865D340006B48E1 /* Gekidou in Frameworks */, + 27C667A529523F0A00E590D5 /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -352,6 +359,14 @@ name = Mattermost; sourceTree = ""; }; + 27C667AB2952425700E590D5 /* ErrorReporting */ = { + isa = PBXGroup; + children = ( + 27C667A8295241B600E590D5 /* Sentry.swift */, + ); + path = ErrorReporting; + sourceTree = ""; + }; 33E107B4DC21A5C48B09F800 /* Pods */ = { isa = PBXGroup; children = ( @@ -625,6 +640,7 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + 27C667AB2952425700E590D5 /* ErrorReporting */, 13B07FAE1A68108700A75B9A /* Mattermost */, 7F581D33221ED5C60099E66B /* NotificationService */, 7F292A701E8AB73400A450A3 /* SplashScreenResource */, @@ -698,6 +714,7 @@ name = NotificationService; packageProductDependencies = ( 49AE370026D4455D00EF4E52 /* Gekidou */, + 27C667A229523ECA00E590D5 /* Sentry */, ); productName = NotificationService; productReference = 7F581D32221ED5C60099E66B /* NotificationService.appex */; @@ -719,6 +736,7 @@ packageProductDependencies = ( 7FD4822B2864D73300A5B18B /* OpenGraph */, 7F4288032865D340006B48E1 /* Gekidou */, + 27C667A429523F0A00E590D5 /* Sentry */, ); productName = MattermostShare; productReference = 7FC5698628563FDB000B0905 /* MattermostShare.appex */; @@ -782,7 +800,8 @@ ); mainGroup = 83CBB9F61A601CBA00E9B192; packageReferences = ( - 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph.git" */, + 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph" */, + 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, ); productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; @@ -1005,6 +1024,7 @@ buildActionMask = 2147483647; files = ( 7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */, + 27C667A9295241B600E590D5 /* Sentry.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1021,6 +1041,7 @@ 7F93AAB8287778090047B89F /* Publishers.swift in Sources */, 7F7E9F732864E8060064BFAF /* CompassIcons.swift in Sources */, 7F93AABA28777A390047B89F /* Notification.swift in Sources */, + 27C667AA295241B600E590D5 /* Sentry.swift in Sources */, 7FA9A9902868BD8800AB35A1 /* LocalFileManager.swift in Sources */, 7F93AA9E2875FD310047B89F /* CancelButton.swift in Sources */, 7F42880A286672F6006B48E1 /* ServerService.swift in Sources */, @@ -1502,7 +1523,15 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph.git" */ = { + 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; + requirement = { + branch = 8.0.0; + kind = branch; + }; + }; + 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/satoshi-takano/OpenGraph.git"; requirement = { @@ -1513,6 +1542,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 27C667A229523ECA00E590D5 /* Sentry */ = { + isa = XCSwiftPackageProductDependency; + package = 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; + }; + 27C667A429523F0A00E590D5 /* Sentry */ = { + isa = XCSwiftPackageProductDependency; + package = 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; + }; 49AE370026D4455D00EF4E52 /* Gekidou */ = { isa = XCSwiftPackageProductDependency; productName = Gekidou; @@ -1527,7 +1566,7 @@ }; 7FD4822B2864D73300A5B18B /* OpenGraph */ = { isa = XCSwiftPackageProductDependency; - package = 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph.git" */; + package = 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph" */; productName = OpenGraph; }; /* End XCSwiftPackageProductDependency section */ diff --git a/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved index 72a8937d2e..edc1406c88 100644 --- a/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,6 +10,15 @@ "version": "1.4.1" } }, + { + "package": "Sentry", + "repositoryURL": "https://github.com/getsentry/sentry-cocoa.git", + "state": { + "branch": "8.0.0", + "revision": "1a18683901844a2970ccfb633e4ebae565361817", + "version": null + } + }, { "package": "SQLite.swift", "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index d738e7e41d..814d39d4c0 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -50,8 +50,8 @@ NSExtensionActivationRule - NSExtensionActivationDictionaryVersion - 2 + NSExtensionActivationDictionaryVersion + 2 NSExtensionActivationSupportsAttachmentsWithMaxCount 10 NSExtensionActivationSupportsFileWithMaxCount @@ -73,5 +73,9 @@ NSExtensionPointIdentifier com.apple.share-services + SENTRY_DSN_IOS + $(SENTRY_DSN_IOS) + SENTRY_ENABLED + $(SENTRY_ENABLED) diff --git a/ios/MattermostShare/ShareViewController.swift b/ios/MattermostShare/ShareViewController.swift index f7e0cc8d58..e70489dd1a 100644 --- a/ios/MattermostShare/ShareViewController.swift +++ b/ios/MattermostShare/ShareViewController.swift @@ -10,6 +10,7 @@ import Gekidou import SwiftUI import UIKit import os.log +import Sentry class ShareViewController: UIViewController { private var fileManager: LocalFileManager? @@ -20,7 +21,6 @@ class ShareViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.isModalInPresentation = true - self.addObservers() fileManager = LocalFileManager() if let inputItems = extensionContext?.inputItems { @@ -34,6 +34,9 @@ class ShareViewController: UIViewController { ) }) } + + // Initialize Sentry + initSentryAppExt() } override func viewDidAppear(_ animated: Bool) { @@ -94,6 +97,8 @@ class ShareViewController: UIViewController { let fileCount = attachments.count let files: [String] = attachments.map{ $0.fileUrl.absoluteString } + + var message = text if linkPreviewUrl != nil && !linkPreviewUrl!.isEmpty { if text.isEmpty { diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index 09ec58b325..9e2224e0aa 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -29,5 +29,9 @@ NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).NotificationService + SENTRY_DSN_IOS + $(SENTRY_DSN_IOS) + SENTRY_ENABLED + $(SENTRY_ENABLED) diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 88d4078316..9f0810ffac 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -9,6 +9,11 @@ class NotificationService: UNNotificationServiceExtension { var retryIndex = 0 + override init() { + super.init() + initSentryAppExt() + } + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler @@ -24,7 +29,6 @@ class NotificationService: UNNotificationServiceExtension { func processResponse(serverUrl: String, data: Data, bestAttemptContent: UNMutableNotificationContent, contentHandler: ((UNNotificationContent) -> Void)?) { bestAttemptContent.userInfo["server_url"] = serverUrl - let json = try? JSONSerialization.jsonObject(with: data) as! [String: Any] if let json = json { if let message = json["message"] as? String { From f32b2dfeb230a638b86965518f54314d82fd6063 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 21 Dec 2022 19:35:34 +0200 Subject: [PATCH 27/71] Perform search when hashtag is pressed (#6893) * Perform search when hashtag is pressed * PM feedback review --- app/components/markdown/hashtag/index.tsx | 10 ++++++++-- app/screens/home/search/search.tsx | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/components/markdown/hashtag/index.tsx b/app/components/markdown/hashtag/index.tsx index 2090e80737..17b041c54c 100644 --- a/app/components/markdown/hashtag/index.tsx +++ b/app/components/markdown/hashtag/index.tsx @@ -2,8 +2,9 @@ // See LICENSE.txt for license information. import React from 'react'; -import {StyleProp, Text, TextStyle} from 'react-native'; +import {DeviceEventEmitter, StyleProp, Text, TextStyle} from 'react-native'; +import {Navigation, Screens} from '@constants'; import {popToRoot, dismissAllModals} from '@screens/navigation'; type HashtagProps = { @@ -17,7 +18,12 @@ const Hashtag = ({hashtag, linkStyle}: HashtagProps) => { await dismissAllModals(); await popToRoot(); - // showSearchModal('#' + hashtag); + DeviceEventEmitter.emit(Navigation.NAVIGATE_TO_TAB, { + screen: Screens.SEARCH, + params: { + searchTerm: hashtag, + }, + }); }; return ( diff --git a/app/screens/home/search/search.tsx b/app/screens/home/search/search.tsx index f10dd256b5..c9aadd2310 100644 --- a/app/screens/home/search/search.tsx +++ b/app/screens/home/search/search.tsx @@ -109,6 +109,14 @@ const SearchScreen = ({teamId, teams}: Props) => { setSearchTeamId(teamId); }, [teamId]); + useEffect(() => { + if (searchTerm) { + resetToInitial(); + setSearchValue(searchTerm); + handleSearch(searchTeamId, searchTerm); + } + }, [searchTerm]); + const onSnap = (offset: number, animated = true) => { scrollRef.current?.scrollToOffset({offset, animated}); }; From bcb8ffa2d17589f940b209e3e9580aaa1778d437 Mon Sep 17 00:00:00 2001 From: Mattermod Date: Wed, 21 Dec 2022 19:37:15 +0200 Subject: [PATCH 28/71] Update Licences at Notice.txt to reflect dependency changes. (#6825) --- NOTICE.txt | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/NOTICE.txt b/NOTICE.txt index 336ae5e2a0..8453b77d56 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -200,6 +200,41 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- + +## @mattermost/react-native-turbo-mailer + +This product contains '@mattermost/react-native-turbo-mailer' by Avinash Lingaloo. + +An adaptation of react-native-mail that supports Turbo Module + +* HOMEPAGE: + * https://github.com/mattermost/react-native-turbo-mailer#readme + +* LICENSE: MIT + +MIT License + +Copyright (c) 2022 Mattermost +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + --- ## @msgpack/msgpack @@ -493,6 +528,21 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--- + +## @react-navigation/stack + +This product contains '@react-navigation/stack'. + +Stack navigator component for iOS and Android with animated transitions and gestures + +* HOMEPAGE: + * https://reactnavigation.org/docs/stack-navigator/ + +* LICENSE: MIT + + + --- ## @rudderstack/rudder-sdk-react-native @@ -1109,6 +1159,40 @@ Lightweight fuzzy-search limitations under the License. +--- + +## html-entities + +This product contains 'html-entities' by Marat Dulin. + +Fastest HTML entities encode/decode library. + +* HOMEPAGE: + * https://github.com/mdevils/html-entities#readme + +* LICENSE: MIT + +Copyright (c) 2021 Dulin Marat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + --- ## jail-monkey From 9da7b47827044b1ccef9a9bb31c452fd9745ecc6 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 21 Dec 2022 20:42:45 +0200 Subject: [PATCH 29/71] Show the current channel when category is collapsed on tablets (#6888) --- app/queries/servers/system.ts | 8 +++++++- .../channel_list/categories_list/categories/body/index.ts | 2 -- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/queries/servers/system.ts b/app/queries/servers/system.ts index 2780a9e276..5306d7e1f2 100644 --- a/app/queries/servers/system.ts +++ b/app/queries/servers/system.ts @@ -404,7 +404,13 @@ export async function setCurrentTeamAndChannelId(operator: ServerDataOperator, t export const observeLastUnreadChannelId = (database: Database): Observable => { return querySystemValue(database, SYSTEM_IDENTIFIERS.LAST_UNREAD_CHANNEL_ID).observe().pipe( switchMap((result) => (result.length ? result[0].observe() : of$({value: ''}))), - switchMap((model) => of$(model.value)), + switchMap((model) => { + if (model.value) { + return of$(model.value); + } + + return observeCurrentChannelId(database); + }), ); }; diff --git a/app/screens/home/channel_list/categories_list/categories/body/index.ts b/app/screens/home/channel_list/categories_list/categories/body/index.ts index 4ec32b9a56..9182d24bb9 100644 --- a/app/screens/home/channel_list/categories_list/categories/body/index.ts +++ b/app/screens/home/channel_list/categories_list/categories/body/index.ts @@ -162,8 +162,6 @@ const enhance = withObservables(['category', 'isTablet', 'locale'], ({category, return { limit, sortedChannels, - notifyProps, - lastUnreadId, unreadsOnTop, unreadIds, category, From a7db26992dcc704c76200ee24e71f27d0da61468 Mon Sep 17 00:00:00 2001 From: Joseph Baylon Date: Wed, 21 Dec 2022 11:31:32 -0800 Subject: [PATCH 30/71] Detox/E2E: Upgrade detox android emulator to api 31 (#6896) --- .gitignore | 2 +- detox/.detoxrc.json | 2 +- detox/android_emulator/config.ini | 10 +++++----- detox/create_android_emulator.sh | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 89f914522b..a597d9eb59 100644 --- a/.gitignore +++ b/.gitignore @@ -102,7 +102,7 @@ coverage mattermost-license.txt *.mattermost-license detox/artifacts -detox/detox_pixel_4_xl_api_30 +detox/detox_pixel_* # Bundle artifact *.jsbundle diff --git a/detox/.detoxrc.json b/detox/.detoxrc.json index fb7bc86c14..5a03db58c8 100644 --- a/detox/.detoxrc.json +++ b/detox/.detoxrc.json @@ -36,7 +36,7 @@ "android.emulator": { "type": "android.emulator", "device": { - "avdName": "detox_pixel_4_xl_api_30" + "avdName": "detox_pixel_4_xl_api_31" } } }, diff --git a/detox/android_emulator/config.ini b/detox/android_emulator/config.ini index bd7703f6f2..7cae4642b6 100644 --- a/detox/android_emulator/config.ini +++ b/detox/android_emulator/config.ini @@ -1,7 +1,7 @@ -AvdId = Detox_Pixel_4_XL_API_30 +AvdId = Detox_Pixel_4_XL_API_31 PlayStore.enabled = false -abi.type = x86 -avd.ini.displayname = Detox Pixel 4 XL API 30 +abi.type = x86_64 +avd.ini.displayname = Detox Pixel 4 XL API 31 avd.ini.encoding = UTF-8 disk.dataPartition.size = 6g fastboot.chosenSnapshotFile = @@ -16,7 +16,7 @@ hw.audioOutput = no hw.battery = yes hw.camera.back = virtualscene hw.camera.front = emulated -hw.cpu.arch = x86 +hw.cpu.arch = x86_64 hw.cpu.ncore = 4 hw.dPad = no hw.device.hash2 = MD5:80326cf5b53c08af25d4243cb231faa9 @@ -36,7 +36,7 @@ hw.sdCard = no hw.sensors.orientation = yes hw.sensors.proximity = yes hw.trackBall = no -image.sysdir.1 = system-images/android-30/google_apis/x86/ +image.sysdir.1 = system-images/android-31/google_apis/x86_64/ runtime.network.latency = none runtime.network.speed = full sdcard.size = 0 diff --git a/detox/create_android_emulator.sh b/detox/create_android_emulator.sh index 1bd6c909e0..380a1a24d5 100755 --- a/detox/create_android_emulator.sh +++ b/detox/create_android_emulator.sh @@ -3,13 +3,13 @@ set -ex set -o pipefail -NAME=detox_pixel_4_xl_api_30 +NAME=detox_pixel_4_xl_api_31 if emulator -list-avds | grep -q $NAME; then echo "'${NAME}' Android virtual device already exists." else - # Create virtual device in a relative "detox_pixel_4_xl_api_30" folder - avdmanager create avd -n $NAME -k 'system-images;android-30;google_apis;x86' -g google_apis -p $NAME -d 'pixel' + # Create virtual device in a relative "detox_pixel_4_xl_api_31" folder + avdmanager create avd -n $NAME -k 'system-images;android-31;google_apis;x86_64' -g google_apis -p $NAME -d 'pixel' # Copy predefined config and skin cp -r android_emulator/ $NAME/ From da4470bd78487fa4005b4fdf7e1ccf48f891192c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Thu, 22 Dec 2022 19:46:11 +0100 Subject: [PATCH 31/71] Allow to select text from tables on full view (#6901) * Allow to select text from tables on full view * Fix check * Remove inline style --- app/components/markdown/markdown.tsx | 9 ++++++++- app/components/markdown/markdown_table_cell/index.tsx | 9 ++++++++- .../__snapshots__/system_message_helpers.test.js.snap | 7 +++++++ app/components/syntax_highlight/renderer.tsx | 2 +- app/screens/code/index.tsx | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx index a7efb3a11b..0437097154 100644 --- a/app/components/markdown/markdown.tsx +++ b/app/components/markdown/markdown.tsx @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {useManagedConfig} from '@mattermost/react-native-emm'; import {Parser, Node} from 'commonmark'; import Renderer from 'commonmark-react-renderer'; import React, {ReactElement, useMemo, useRef} from 'react'; @@ -131,6 +132,7 @@ const Markdown = ({ textStyles = {}, theme, value = '', baseParagraphStyle, }: MarkdownProps) => { const style = getStyleSheet(theme); + const managedConfig = useManagedConfig(); const urlFilter = (url: string) => { const scheme = getScheme(url); @@ -471,10 +473,14 @@ const Markdown = ({ }; const renderText = ({context, literal}: MarkdownBaseRenderer) => { + const selectable = (managedConfig.copyAndPasteProtection !== 'true') && context.includes('table_cell'); if (context.indexOf('image') !== -1) { // If this text is displayed, it will be styled by the image component return ( - + {literal} ); @@ -496,6 +502,7 @@ const Markdown = ({ {literal} diff --git a/app/components/markdown/markdown_table_cell/index.tsx b/app/components/markdown/markdown_table_cell/index.tsx index 2da5759471..3fb8eb7173 100644 --- a/app/components/markdown/markdown_table_cell/index.tsx +++ b/app/components/markdown/markdown_table_cell/index.tsx @@ -24,6 +24,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { justifyContent: 'flex-start', padding: 8, }, + textContainer: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + }, cellRightBorder: { borderRightWidth: 1, }, @@ -57,7 +62,9 @@ const MarkdownTableCell = ({isLastCell, align, children}: MarkdownTableCellProps style={[cellStyle, textStyle]} testID='markdown_table_cell' > - {children} + + {children} + ); }; diff --git a/app/components/post_list/post/system_message/__snapshots__/system_message_helpers.test.js.snap b/app/components/post_list/post/system_message/__snapshots__/system_message_helpers.test.js.snap index 04c7ab01b1..8d44565568 100644 --- a/app/components/post_list/post/system_message/__snapshots__/system_message_helpers.test.js.snap +++ b/app/components/post_list/post/system_message/__snapshots__/system_message_helpers.test.js.snap @@ -44,6 +44,7 @@ exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] = {value} diff --git a/app/screens/code/index.tsx b/app/screens/code/index.tsx index 991360a6e8..a237bd0285 100644 --- a/app/screens/code/index.tsx +++ b/app/screens/code/index.tsx @@ -31,7 +31,7 @@ const Code = ({code, language, textStyle}: Props) => { From 49c242e300adf1c28235551581fe43eabb5d65d3 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 22 Dec 2022 20:46:19 +0200 Subject: [PATCH 32/71] Fix race condition for keyboard tracker (#6902) --- app/hooks/keyboard_tracking.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/hooks/keyboard_tracking.ts b/app/hooks/keyboard_tracking.ts index ad1b8670c1..95da896846 100644 --- a/app/hooks/keyboard_tracking.ts +++ b/app/hooks/keyboard_tracking.ts @@ -24,10 +24,13 @@ export const useKeyboardTrackingPaused = (keyboardTrackingRef: RefObject { - if (!isPostDraftPaused.current) { - isPostDraftPaused.current = true; - keyboardTrackingRef.current?.pauseTracking(trackerId); - } + setTimeout(() => { + const visibleScreen = NavigationStore.getVisibleScreen(); + if (!isPostDraftPaused.current && !screens.includes(visibleScreen)) { + isPostDraftPaused.current = true; + keyboardTrackingRef.current?.pauseTracking(trackerId); + } + }); }); const commandCompletedListener = Navigation.events().registerCommandCompletedListener(() => { From 9753334ff20a96e9f5d4408cc8cc7edcde1359d7 Mon Sep 17 00:00:00 2001 From: Matthew Birtch Date: Fri, 23 Dec 2022 07:08:51 -0500 Subject: [PATCH 33/71] [Gekidou MM-46365] fix reaction bar space and update bottom sheet styles (#6634) * updated styles for reaction bar and made pick reaction pressable * removed unused style import * removed unused style import * updated user avatars, user presence, thread options bottom sheets to new style also * updated status bottom sheet, thread options, and some tweaks to the bottom sheet main file * fixed a few minor styling issues * used proper bottom inset in user presence bottom sheet * various updates to bottom sheets * used negative top position instead of negative margin * updated camera type bottom sheet * further refinements to bottom sheets * updates to emoji bar and profile image picker Co-authored-by: Elias Nahum --- app/components/option_item/index.tsx | 1 - .../camera_quick_action/camera_type.tsx | 96 +++++-------------- .../camera_quick_action/index.tsx | 12 ++- .../post_priority_picker/index.tsx | 2 +- app/components/user_avatars_stack/index.tsx | 15 ++- app/components/user_item/user_item.tsx | 4 +- app/constants/reaction_picker.ts | 2 +- app/screens/bottom_sheet/button.tsx | 4 +- app/screens/bottom_sheet/content.tsx | 5 +- app/screens/bottom_sheet/index.tsx | 9 +- .../components/custom_status_input.tsx | 2 +- app/screens/custom_status/index.tsx | 2 +- .../components/profile_image_picker.tsx | 26 ++++- .../custom_label/custom_status_text.tsx | 2 +- .../options/custom_status/index.tsx | 2 +- .../options/user_presence/index.tsx | 39 ++++++-- .../servers/servers_list/index.tsx | 2 +- .../servers_list/server_item/server_item.tsx | 1 + app/screens/home/search/results/filter.tsx | 3 +- .../reaction_bar/pick_reaction/index.tsx | 48 ++++++---- .../reaction_bar/reaction/index.tsx | 3 +- .../reaction_bar/reaction_bar.tsx | 6 +- app/screens/reactions/emoji_bar/index.tsx | 5 +- .../reactors_list/reactor/reactor.tsx | 6 ++ app/screens/thread_options/thread_options.tsx | 21 ++-- assets/base/i18n/en.json | 18 ++-- 26 files changed, 188 insertions(+), 148 deletions(-) diff --git a/app/components/option_item/index.tsx b/app/components/option_item/index.tsx index dfd8ea3154..96a5a881b9 100644 --- a/app/components/option_item/index.tsx +++ b/app/components/option_item/index.tsx @@ -283,7 +283,6 @@ const OptionItem = ({ } ); - if (Object.values(TouchableOptionTypes).includes(type)) { return ( diff --git a/app/components/post_draft/quick_actions/camera_quick_action/camera_type.tsx b/app/components/post_draft/quick_actions/camera_quick_action/camera_type.tsx index 411c4d5945..c42eccb1d4 100644 --- a/app/components/post_draft/quick_actions/camera_quick_action/camera_type.tsx +++ b/app/components/post_draft/quick_actions/camera_quick_action/camera_type.tsx @@ -2,60 +2,36 @@ // See LICENSE.txt for license information. import React from 'react'; +import {useIntl} from 'react-intl'; import {View} from 'react-native'; import {CameraOptions} from 'react-native-image-picker'; -import CompassIcon from '@components/compass_icon'; import FormattedText from '@components/formatted_text'; -import TouchableWithFeedback from '@components/touchable_with_feedback'; +import SlideUpPanelItem from '@components/slide_up_panel_item'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {dismissBottomSheet} from '@screens/navigation'; import {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; type Props = { onPress: (options: CameraOptions) => void; } const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({ - center: { - alignItems: 'center', - }, - container: { - alignItems: 'center', - backgroundColor: theme.centerChannelBg, - height: 200, - paddingVertical: 10, - }, - flex: { - flex: 1, - }, - options: { - alignItems: 'center', - flex: 1, - flexDirection: 'row', - justifyContent: 'space-evenly', - width: '100%', - marginBottom: 50, - }, - optionContainer: { - alignItems: 'flex-start', - }, title: { color: theme.centerChannelColor, - fontSize: 18, - fontWeight: 'bold', - }, - text: { - color: theme.centerChannelColor, - fontSize: 15, + ...typography('Heading', 600, 'SemiBold'), + marginBottom: 8, }, + })); const CameraType = ({onPress}: Props) => { const theme = useTheme(); const isTablet = useIsTablet(); const style = getStyle(theme); + const intl = useIntl(); const onPhoto = async () => { const options: CameraOptions = { @@ -80,54 +56,26 @@ const CameraType = ({onPress}: Props) => { }; return ( - + {!isTablet && } - - - - - - - - - - - - - - - - - - + + ); }; diff --git a/app/components/post_draft/quick_actions/camera_quick_action/index.tsx b/app/components/post_draft/quick_actions/camera_quick_action/index.tsx index 2579bb6ade..35d24aeaa5 100644 --- a/app/components/post_draft/quick_actions/camera_quick_action/index.tsx +++ b/app/components/post_draft/quick_actions/camera_quick_action/index.tsx @@ -5,14 +5,18 @@ import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; import {Alert, StyleSheet} from 'react-native'; import {CameraOptions} from 'react-native-image-picker'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; import CompassIcon from '@components/compass_icon'; +import {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {ICON_SIZE} from '@constants/post_draft'; import {useTheme} from '@context/theme'; +import {TITLE_HEIGHT} from '@screens/bottom_sheet/content'; import {bottomSheet} from '@screens/navigation'; import {fileMaxWarning} from '@utils/file'; import PickerUtil from '@utils/file/file_picker'; +import {bottomSheetSnapPoint} from '@utils/helpers'; import {changeOpacity} from '@utils/theme'; import CameraType from './camera_type'; @@ -36,6 +40,7 @@ export default function CameraQuickAction({ }: QuickActionAttachmentProps) { const intl = useIntl(); const theme = useTheme(); + const {bottom} = useSafeAreaInsets(); const handleButtonPress = useCallback((options: CameraOptions) => { const picker = new PickerUtil(intl, @@ -64,14 +69,15 @@ export default function CameraQuickAction({ return; } + const snap = bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom); bottomSheet({ - title: intl.formatMessage({id: 'camera_type.title', defaultMessage: 'Choose an action'}), + title: intl.formatMessage({id: 'mobile.camera_type.title', defaultMessage: 'Camera options'}), renderContent, - snapPoints: [200, 10], + snapPoints: [TITLE_HEIGHT + snap, 10], theme, closeButtonId: 'camera-close-id', }); - }, [intl, theme, renderContent, maxFilesReached, maxFileCount]); + }, [intl, theme, renderContent, maxFilesReached, maxFileCount, bottom]); const actionTestID = disabled ? `${testID}.disabled` : testID; const color = disabled ? changeOpacity(theme.centerChannelColor, 0.16) : changeOpacity(theme.centerChannelColor, 0.64); diff --git a/app/components/post_priority/post_priority_picker/index.tsx b/app/components/post_priority/post_priority_picker/index.tsx index 48c51e60a1..5b70622b8f 100644 --- a/app/components/post_priority/post_priority_picker/index.tsx +++ b/app/components/post_priority/post_priority_picker/index.tsx @@ -30,7 +30,7 @@ const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({ }, title: { color: theme.centerChannelColor, - ...typography('Body', 600, 'SemiBold'), + ...typography('Heading', 600, 'SemiBold'), }, betaContainer: { backgroundColor: PostPriorityColors.IMPORTANT, diff --git a/app/components/user_avatars_stack/index.tsx b/app/components/user_avatars_stack/index.tsx index d2a43bc323..57199ba305 100644 --- a/app/components/user_avatars_stack/index.tsx +++ b/app/components/user_avatars_stack/index.tsx @@ -4,11 +4,14 @@ import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; import {StyleProp, Text, TouchableOpacity, View, ViewStyle} from 'react-native'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; import FormattedText from '@components/formatted_text'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; +import {TITLE_HEIGHT} from '@screens/bottom_sheet/content'; import {bottomSheet} from '@screens/navigation'; +import {bottomSheetSnapPoint} from '@utils/helpers'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -19,6 +22,7 @@ import UsersList from './users_list'; import type UserModel from '@typings/database/models/servers/user'; const OVERFLOW_DISPLAY_LIMIT = 99; +const USER_ROW_HEIGHT = 40; type Props = { channelId: string; @@ -88,9 +92,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { marginBottom: 12, }, listHeaderText: { - color: changeOpacity(theme.centerChannelColor, 0.56), - ...typography('Body', 75, 'SemiBold'), - textTransform: 'uppercase', + color: theme.centerChannelColor, + ...typography('Heading', 600, 'SemiBold'), }, }; }); @@ -99,6 +102,7 @@ const UserAvatarsStack = ({breakAt = 3, channelId, location, style: baseContaine const theme = useTheme(); const intl = useIntl(); const isTablet = useIsTablet(); + const {bottom} = useSafeAreaInsets(); const showParticipantsList = useCallback(preventDoubleTap(() => { const renderContent = () => ( @@ -119,15 +123,16 @@ const UserAvatarsStack = ({breakAt = 3, channelId, location, style: baseContaine /> ); + const snap = bottomSheetSnapPoint(Math.min(users.length, 5), USER_ROW_HEIGHT, bottom); bottomSheet({ closeButtonId: 'close-set-user-status', renderContent, initialSnapIndex: 1, - snapPoints: ['90%', '50%', 10], + snapPoints: ['90%', TITLE_HEIGHT + snap, 10], title: intl.formatMessage({id: 'mobile.participants.header', defaultMessage: 'Thread Participants'}), theme, }); - }), [isTablet, theme, users, channelId, location]); + }), [isTablet, theme, users, channelId, location, bottom]); const displayUsers = users.slice(0, breakAt); const overflowUsersCount = Math.min(users.length - displayUsers.length, OVERFLOW_DISPLAY_LIMIT); diff --git a/app/components/user_item/user_item.tsx b/app/components/user_item/user_item.tsx index f73447fcff..a16e3a443b 100644 --- a/app/components/user_item/user_item.tsx +++ b/app/components/user_item/user_item.tsx @@ -25,6 +25,7 @@ type AtMentionItemProps = { showFullName: boolean; testID?: string; isCustomStatusEnabled: boolean; + pictureContainerStyle?: StyleProp; } const getName = (user: UserProfile | UserModel | undefined, showFullName: boolean, isCurrentUser: boolean, intl: IntlShape) => { @@ -95,6 +96,7 @@ const UserItem = ({ showFullName, testID, isCustomStatusEnabled, + pictureContainerStyle, }: AtMentionItemProps) => { const theme = useTheme(); const style = getStyleFromTheme(theme); @@ -116,7 +118,7 @@ const UserItem = ({ style={[style.row, containerStyle]} testID={userItemTestId} > - + { }, separator: { height: 1, - right: 16, + right: 20, borderTopWidth: 1, borderColor: changeOpacity(theme.centerChannelColor, 0.08), + marginBottom: 20, }, }; }); @@ -83,7 +84,7 @@ const BottomSheetContent = ({buttonText, buttonIcon, children, disableButton, on {showButton && ( <> - +