diff --git a/.circleci/config.yml b/.circleci/config.yml index 0c57d7508..ba6e2ee29 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,7 @@ commands: steps: - add_ssh_keys: fingerprints: - - "59:4d:99:5e:1c:6d:30:36:6d:60:76:88:ff:a7:ab:63" + - "03:1c:a7:07:35:bc:57:e4:1d:6c:e1:2c:4b:be:09:6d" - run: name: Clone the mobile private repo command: git clone git@github.com:mattermost/mattermost-mobile-private.git ~/mattermost-mobile-private @@ -309,13 +309,13 @@ jobs: - save: filename: "*.apk" - # build-android-release: - # executor: android - # steps: - # - build-android - # - persist - # - save: - # filename: "*.apk" + build-android-release: + executor: android + steps: + - build-android + - persist + - save: + filename: "*.apk" build-android-pr: executor: android @@ -326,27 +326,27 @@ jobs: - save: filename: "*.apk" - # build-android-unsigned: - # executor: android - # steps: - # - checkout: - # path: ~/mattermost-mobile - # - npm-dependencies - # - assets - # - fastlane-dependencies: - # for: android - # - gradle-dependencies - # - run: - # name: Jetify Android libraries - # command: ./node_modules/.bin/jetify - # - run: - # working_directory: fastlane - # name: Run fastlane to build unsigned android - # no_output_timeout: 30m - # command: bundle exec fastlane android unsigned - # - persist - # - save: - # filename: "*.apk" + build-android-unsigned: + executor: android + steps: + - checkout: + path: ~/mattermost-mobile + - npm-dependencies + - assets + - fastlane-dependencies: + for: android + - gradle-dependencies + - run: + name: Jetify Android libraries + command: ./node_modules/.bin/jetify + - run: + working_directory: fastlane + name: Run fastlane to build unsigned android + no_output_timeout: 30m + command: bundle exec fastlane android unsigned + - persist + - save: + filename: "*.apk" build-ios-beta: executor: @@ -358,13 +358,13 @@ jobs: - save: filename: "*.ipa" - # build-ios-release: - # executor: ios - # steps: - # - build-ios - # - persist - # - save: - # filename: "*.ipa" + build-ios-release: + executor: ios + steps: + - build-ios + - persist + - save: + filename: "*.ipa" build-ios-pr: executor: ios @@ -375,63 +375,64 @@ jobs: - save: filename: "*.ipa" - # build-ios-unsigned: - # executor: ios - # steps: - # - checkout: - # path: ~/mattermost-mobile - # - npm-dependencies - # - pods-dependencies - # - assets - # - fastlane-dependencies: - # for: ios - # - run: - # working_directory: fastlane - # name: Run fastlane to build unsigned iOS - # no_output_timeout: 30m - # command: | - # HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman - # bundle exec fastlane ios unsigned - # - persist_to_workspace: - # root: ~/ - # paths: - # - mattermost-mobile/*.ipa - # - save: - # filename: "*.ipa" + build-ios-unsigned: + executor: ios + steps: + - checkout: + path: ~/mattermost-mobile + - npm-dependencies + - pods-dependencies + - assets + - fastlane-dependencies: + for: ios + - run: + working_directory: fastlane + name: Run fastlane to build unsigned iOS + no_output_timeout: 30m + command: | + HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman + bundle exec fastlane ios unsigned + - persist_to_workspace: + root: ~/ + paths: + - mattermost-mobile/*.ipa + - save: + filename: "*.ipa" - # build-ios-simulator: - # executor: ios - # steps: - # - checkout: - # path: ~/mattermost-mobile - # - npm-dependencies - # - pods-dependencies - # - assets - # - fastlane-dependencies: - # for: ios - # - run: - # working_directory: fastlane - # name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator - # no_output_timeout: 30m - # command: | - # HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman - # bundle exec fastlane ios simulator - # - persist_to_workspace: - # root: ~/ - # paths: - # - mattermost-mobile/Mattermost-simulator-x86_64.app.zip - # - save: - # filename: "Mattermost-simulator-x86_64.app.zip" + build-ios-simulator: + executor: ios + steps: + - checkout: + path: ~/mattermost-mobile + - npm-dependencies + - pods-dependencies + - assets + - fastlane-dependencies: + for: ios + - run: + working_directory: fastlane + name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator + no_output_timeout: 30m + command: | + HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman + bundle exec fastlane ios simulator + - persist_to_workspace: + root: ~/ + paths: + - mattermost-mobile/Mattermost-simulator-x86_64.app.zip + - save: + filename: "Mattermost-simulator-x86_64.app.zip" - # deploy-android-release: - # executor: - # name: android - # resource_class: medium - # steps: - # - deploy-to-store: - # task: "Deploy to Google Play" - # target: android - # file: "*.apk" + deploy-android-release: + executor: + name: android + resource_class: medium + steps: + - deploy-to-store: + task: "Deploy to Google Play" + target: android + file: "*.apk" + env: "SUPPLY_TRACK=beta" deploy-android-beta: executor: @@ -444,13 +445,14 @@ jobs: file: "*.apk" env: "SUPPLY_TRACK=alpha" - # deploy-ios-release: - # executor: ios - # steps: - # - deploy-to-store: - # task: "Deploy to TestFlight" - # target: ios - # file: "*.ipa" + deploy-ios-release: + executor: ios + steps: + - deploy-to-store: + task: "Deploy to TestFlight" + target: ios + file: "*.ipa" + env: "" deploy-ios-beta: executor: ios @@ -461,17 +463,17 @@ jobs: file: "*.ipa" env: "" - # github-release: - # executor: - # name: android - # resource_class: medium - # steps: - # - attach_workspace: - # at: ~/ - # - run: - # name: Create GitHub release - # working_directory: fastlane - # command: bundle exec fastlane github + github-release: + executor: + name: android + resource_class: medium + steps: + - attach_workspace: + at: ~/ + - run: + name: Create GitHub release + working_directory: fastlane + command: bundle exec fastlane github workflows: version: 2 @@ -483,26 +485,24 @@ workflows: # requires: # - test - # - build-android-release: - # context: mattermost-mobile-android-release - # requires: - # - test - # filters: - # branches: - # only: - # - /^build-\d+$/ - # - /^build-android-\d+$/ - # - /^build-android-release-\d+$/ - # - deploy-android-release: - # context: mattermost-mobile-android-release - # requires: - # - build-android-release - # filters: - # branches: - # only: - # - /^build-\d+$/ - # - /^build-android-\d+$/ - # - /^build-android-release-\d+$/ + - build-android-release: + context: mattermost-mobile-android-release + requires: + - test + filters: + branches: + only: + - /^build-release-\d+$/ + - /^build-android-release-\d+$/ + - deploy-android-release: + context: mattermost-mobile-android-release + requires: + - build-android-release + filters: + branches: + only: + - /^build-release-\d+$/ + - /^build-android-release-\d+$/ - build-android-beta: context: mattermost-mobile-android-beta @@ -523,26 +523,24 @@ workflows: - /^build-android-\d+$/ - /^build-android-beta-\d+$/ - # - build-ios-release: - # context: mattermost-mobile-ios-release - # requires: - # - test - # filters: - # branches: - # only: - # - /^build-\d+$/ - # - /^build-ios-\d+$/ - # - /^build-ios-release-\d+$/ - # - deploy-ios-release: - # context: mattermost-mobile-ios-release - # requires: - # - build-ios-release - # filters: - # branches: - # only: - # - /^build-\d+$/ - # - /^build-ios-\d+$/ - # - /^build-ios-release-\d+$/ + - build-ios-release: + context: mattermost-mobile-ios-release + requires: + - test + filters: + branches: + only: + - /^build-release-\d+$/ + - /^build-ios-release-\d+$/ + - deploy-ios-release: + context: mattermost-mobile-ios-release + requires: + - build-ios-release + filters: + branches: + only: + - /^build-release-\d+$/ + - /^build-ios-release-\d+$/ - build-ios-beta: context: mattermost-mobile-ios-beta @@ -578,43 +576,41 @@ workflows: branches: only: /^(build|ios)-pr-.*/ - # - build-android-unsigned: - # context: mattermost-mobile-unsigned - # requires: - # - test - # filters: - # tags: - # only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ - # branches: - # only: unsigned - # - build-ios-unsigned: - # context: mattermost-mobile-unsigned - # requires: - # - test - # filters: - # tags: - # only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ - # branches: - # only: unsigned - # - build-ios-simulator: - # context: mattermost-mobile-unsigned - # requires: - # - test - # filters: - # branches: - # only: - # - /^build-\d+$/ - # - /^build-ios-\d+$/ - # - /^build-ios-beta-\d+$/ - # - /^build-ios-sim-\d+$/ + - build-android-unsigned: + context: mattermost-mobile-unsigned + requires: + - test + filters: + tags: + only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ + branches: + only: unsigned + - build-ios-unsigned: + context: mattermost-mobile-unsigned + requires: + - test + filters: + tags: + only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ + branches: + only: unsigned + - build-ios-simulator: + context: mattermost-mobile-unsigned + requires: + - test + filters: + branches: + only: + - /^build-\d+$/ + - /^build-ios-sim-\d+$/ - # - github-release: - # context: mattermost-mobile-unsigned - # requires: - # - build-android-unsigned - # - build-ios-unsigned - # filters: - # tags: - # only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ - # branches: - # only: unsigned + - github-release: + context: mattermost-mobile-unsigned + requires: + - build-android-unsigned + - build-ios-unsigned + filters: + tags: + only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ + branches: + only: unsigned diff --git a/NOTICE.txt b/NOTICE.txt index 8453b77d5..c4a1b9f94 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -2962,28 +2962,6 @@ IN THE SOFTWARE. """ ---- - -## reanimated-bottom-sheet - -This product contains a modified version of 'reanimated-bottom-sheet' by Michał Osadnik. - -Highly configurable component imitating native bottom sheet behavior, with fully native 60 FPS animations! - -* HOMEPAGE: - * https://github.com/osdnk/react-native-reanimated-bottom-sheet - -* LICENSE: MIT - -Copyright 2019 – present Michał Osadnik - -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. - - --- ## semver diff --git a/android/app/build.gradle b/android/app/build.gradle index d9e1d91f6..e58e62f6f 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 446 + versionCode 452 versionName "2.0.0" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java index 77df51b6e..30c7fb286 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.text.TextUtils; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; @@ -20,8 +21,12 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; + +import com.mattermost.helpers.Credentials; +import com.reactlibrary.createthumbnail.CreateThumbnailModule; import com.mattermost.helpers.RealPathUtil; import java.io.File; @@ -29,6 +34,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.ref.WeakReference; +import java.net.URL; import java.nio.channels.FileChannel; public class MattermostManagedModule extends ReactContextBaseJavaModule { @@ -206,6 +212,30 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule { } } + @ReactMethod + public void createThumbnail(ReadableMap options, Promise promise) { + try { + WritableMap optionsMap = Arguments.createMap(); + optionsMap.merge(options); + String url = options.hasKey("url") ? options.getString("url") : ""; + URL videoUrl = new URL(url); + String serverUrl = videoUrl.getProtocol() + "://" + videoUrl.getHost() + ":" + videoUrl.getPort(); + String token = Credentials.getCredentialsForServerSync(this.reactContext, serverUrl); + if (!TextUtils.isEmpty(token)) { + WritableMap headers = Arguments.createMap(); + if (optionsMap.hasKey("headers")) { + headers.merge(optionsMap.getMap("headers")); + } + headers.putString("Authorization", "Bearer " + token); + optionsMap.putMap("headers", headers); + } + CreateThumbnailModule thumb = new CreateThumbnailModule(this.reactContext); + thumb.create(optionsMap.copy(), promise); + } catch (Exception e) { + promise.reject("CreateThumbnail_ERROR", e); + } + } + private static class SaveDataTask extends GuardedResultAsyncTask { private final WeakReference weakContext; private final String fromFile; diff --git a/app/actions/app/global.ts b/app/actions/app/global.ts index e0c5840c3..370231841 100644 --- a/app/actions/app/global.ts +++ b/app/actions/app/global.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Tutorial} from '@constants'; import {GLOBAL_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; import {logError} from '@utils/log'; @@ -22,16 +23,20 @@ export const storeDeviceToken = async (token: string, prepareRecordsOnly = false return storeGlobal(GLOBAL_IDENTIFIERS.DEVICE_TOKEN, token, prepareRecordsOnly); }; -export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => { - return storeGlobal(GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, 'true', prepareRecordsOnly); -}; - export const storeOnboardingViewedValue = async (value = true) => { return storeGlobal(GLOBAL_IDENTIFIERS.ONBOARDING, value, false); }; +export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => { + return storeGlobal(Tutorial.MULTI_SERVER, 'true', prepareRecordsOnly); +}; + export const storeProfileLongPressTutorial = async (prepareRecordsOnly = false) => { - return storeGlobal(GLOBAL_IDENTIFIERS.PROFILE_LONG_PRESS_TUTORIAL, 'true', prepareRecordsOnly); + return storeGlobal(Tutorial.PROFILE_LONG_PRESS, 'true', prepareRecordsOnly); +}; + +export const storeSkinEmojiSelectorTutorial = async (prepareRecordsOnly = false) => { + return storeGlobal(Tutorial.EMOJI_SKIN_SELECTOR, 'true', prepareRecordsOnly); }; export const storeDontAskForReview = async (prepareRecordsOnly = false) => { diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index 03ab9403f..be4c2e219 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -76,7 +76,7 @@ export async function switchToChannel(serverUrl: string, channelId: string, team } models = (await Promise.all(modelPromises)).flat(); - const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, true); + const {member: viewedAt} = await markChannelAsViewed(serverUrl, channelId, false, true); if (viewedAt) { models.push(viewedAt); } @@ -160,7 +160,7 @@ export async function selectAllMyChannelIds(serverUrl: string) { } } -export async function markChannelAsViewed(serverUrl: string, channelId: string, prepareRecordsOnly = false) { +export async function markChannelAsViewed(serverUrl: string, channelId: string, onlyCounts = false, prepareRecordsOnly = false) { try { const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const member = await getMyChannel(database, channelId); @@ -172,8 +172,10 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string, m.isUnread = false; m.mentionsCount = 0; m.manuallyUnread = false; - m.viewedAt = member.lastViewedAt; - m.lastViewedAt = Date.now(); + if (!onlyCounts) { + m.viewedAt = member.lastViewedAt; + m.lastViewedAt = Date.now(); + } }); PushNotifications.removeChannelNotifications(serverUrl, channelId); if (!prepareRecordsOnly) { diff --git a/app/actions/remote/custom_emoji.ts b/app/actions/remote/custom_emoji.ts index 1bdb6312d..cd034828b 100644 --- a/app/actions/remote/custom_emoji.ts +++ b/app/actions/remote/custom_emoji.ts @@ -87,10 +87,17 @@ const debouncedFetchEmojiByNames = debounce(async (serverUrl: string) => { promises.push(client.getCustomEmojiByName(name)); } - const emojis = await Promise.all(promises); - try { - await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false}); + const emojisResult = await Promise.allSettled(promises); + const emojis = emojisResult.reduce((result, e) => { + if (e.status === 'fulfilled') { + result.push(e.value); + } + return result; + }, []); + if (emojis.length) { + await operator.handleCustomEmojis({emojis, prepareRecordsOnly: false}); + } return {error: undefined}; } catch (error) { return {error}; diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index a68e9dafe..58d021009 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -8,6 +8,7 @@ import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnecte import {getCurrentUser} from '@queries/servers/user'; import {setTeamLoading} from '@store/team_load_store'; import {deleteV1Data} from '@utils/file'; +import {isTablet} from '@utils/helpers'; import {logInfo} from '@utils/log'; import {handleEntryAfterLoadNavigation, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common'; @@ -29,7 +30,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) // clear lastUnreadChannelId const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''}); if (removeLastUnreadChannelId) { - operator.batchRecords(removeLastUnreadChannelId); + await operator.batchRecords(removeLastUnreadChannelId); } const {database} = operator; @@ -47,7 +48,12 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData; if (isUpgrade && meData?.user) { - const me = await prepareCommonSystemValues(operator, {currentUserId: meData.user.id}); + const isTabletDevice = await isTablet(); + const me = await prepareCommonSystemValues(operator, { + currentUserId: meData.user.id, + currentTeamId: initialTeamId, + currentChannelId: isTabletDevice ? initialChannelId : undefined, + }); if (me?.length) { await operator.batchRecords(me); } @@ -84,8 +90,8 @@ export async function upgradeEntry(serverUrl: string) { const error = configAndLicense.error || entryData.error; if (!error) { - DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId); - DatabaseManager.setActiveServerDatabase(serverUrl); + await DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId); + await DatabaseManager.setActiveServerDatabase(serverUrl); deleteV1Data(); } diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index 61968e3b1..ba85a9240 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -436,7 +436,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos await operator.batchRecords(models); } catch (error) { - logError('FETCH AUTHORS ERROR', error); + logError('FETCH POSTS BEFORE ERROR', error); } } @@ -544,9 +544,15 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn } if (promises.length) { - const result = await Promise.all(promises); - const authors = result.flat(); + const authorsResult = await Promise.allSettled(promises); + const result = authorsResult.reduce((acc, item) => { + if (item.status === 'fulfilled') { + acc.push(item.value); + } + return acc; + }, []); + const authors = result.flat(); if (!fetchOnly && authors.length) { await operator.handleUsers({ users: authors, diff --git a/app/actions/remote/preference.ts b/app/actions/remote/preference.ts index 8d7c79f81..3180e476a 100644 --- a/app/actions/remote/preference.ts +++ b/app/actions/remote/preference.ts @@ -178,3 +178,19 @@ export const setDirectChannelVisible = async (serverUrl: string, channelId: stri return {error}; } }; + +export const savePreferredSkinTone = async (serverUrl: string, skinCode: string) => { + try { + const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const userId = await getCurrentUserId(database); + const pref: PreferenceType = { + user_id: userId, + category: Preferences.CATEGORY_EMOJI, + name: Preferences.EMOJI_SKINTONE, + value: skinCode, + }; + return savePreference(serverUrl, [pref]); + } catch (error) { + return {error}; + } +}; diff --git a/app/actions/websocket/channel.ts b/app/actions/websocket/channel.ts index e765f17e0..189c7f7d1 100644 --- a/app/actions/websocket/channel.ts +++ b/app/actions/websocket/channel.ts @@ -128,7 +128,7 @@ export async function handleChannelViewedEvent(serverUrl: string, msg: any) { const currentChannelId = await getCurrentChannelId(database); if (activeServerUrl !== serverUrl || (currentChannelId !== channelId && !EphemeralStore.isSwitchingToChannel(channelId))) { - await markChannelAsViewed(serverUrl, channelId, false); + await markChannelAsViewed(serverUrl, channelId); } } catch { // do nothing diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 88d3145f1..6fc0f370e 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -444,7 +444,7 @@ async function fetchPostDataIfNeeded(serverUrl: string) { await fetchPostsForChannel(serverUrl, currentChannelId); markChannelAsRead(serverUrl, currentChannelId); if (!EphemeralStore.wasNotificationTapped()) { - markChannelAsViewed(serverUrl, currentChannelId); + markChannelAsViewed(serverUrl, currentChannelId, true); } EphemeralStore.setNotificationTapped(false); } diff --git a/app/actions/websocket/posts.ts b/app/actions/websocket/posts.ts index 88e5ea87c..1c5877e18 100644 --- a/app/actions/websocket/posts.ts +++ b/app/actions/websocket/posts.ts @@ -143,7 +143,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag markChannelAsRead(serverUrl, post.channel_id); } else if (markAsViewed) { preparedMyChannelHack(myChannel); - const {member: viewedAt} = await markChannelAsViewed(serverUrl, post.channel_id, true); + const {member: viewedAt} = await markChannelAsViewed(serverUrl, post.channel_id, false, true); if (viewedAt) { models.push(viewedAt); } @@ -164,8 +164,13 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag } } + let actionType: string = ActionType.POSTS.RECEIVED_NEW; + if (isCRTEnabled && post.root_id) { + actionType = ActionType.POSTS.RECEIVED_IN_THREAD; + } + const postModels = await operator.handlePosts({ - actionType: ActionType.POSTS.RECEIVED_NEW, + actionType, order: [post.id], posts: [post], prepareRecordsOnly: true, @@ -203,8 +208,14 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage) models.push(...authorsModels); } + let actionType: string = ActionType.POSTS.RECEIVED_NEW; + const isCRTEnabled = await getIsCRTEnabled(operator.database); + if (isCRTEnabled && post.root_id) { + actionType = ActionType.POSTS.RECEIVED_IN_THREAD; + } + const postModels = await operator.handlePosts({ - actionType: ActionType.POSTS.RECEIVED_NEW, + actionType, order: [post.id], posts: [post], prepareRecordsOnly: true, diff --git a/app/actions/websocket/threads.ts b/app/actions/websocket/threads.ts index c3e7f119b..7ccea7225 100644 --- a/app/actions/websocket/threads.ts +++ b/app/actions/websocket/threads.ts @@ -2,12 +2,22 @@ // See LICENSE.txt for license information. import {markTeamThreadsAsRead, processReceivedThreads, updateThread} from '@actions/local/thread'; +import {getCurrentTeamId} from '@app/queries/servers/system'; +import DatabaseManager from '@database/manager'; import EphemeralStore from '@store/ephemeral_store'; export async function handleThreadUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise { try { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return; + } const thread: Thread = JSON.parse(msg.data.thread); - const teamId = msg.broadcast.team_id; + let teamId = msg.broadcast.team_id; + + if (!teamId) { + teamId = await getCurrentTeamId(database); + } // Mark it as following thread.is_following = true; diff --git a/app/client/websocket/index.ts b/app/client/websocket/index.ts index b5fb3c560..80ec31955 100644 --- a/app/client/websocket/index.ts +++ b/app/client/websocket/index.ts @@ -33,6 +33,7 @@ export default class WebSocketClient { private firstConnectCallback?: () => void; private missedEventsCallback?: () => void; private reconnectCallback?: () => void; + private reliableReconnectCallback?: () => void; private errorCallback?: Function; private closeCallback?: (connectFailCount: number, lastDisconnect: number) => void; private connectingCallback?: () => void; @@ -148,8 +149,11 @@ export default class WebSocketClient { logInfo('websocket re-established connection to', this.url); if (!reliableWebSockets && this.reconnectCallback) { this.reconnectCallback(); - } else if (reliableWebSockets && this.serverSequence && this.missedEventsCallback) { - this.missedEventsCallback(); + } else if (reliableWebSockets) { + this.reliableReconnectCallback?.(); + if (this.serverSequence && this.missedEventsCallback) { + this.missedEventsCallback(); + } } } else if (this.firstConnectCallback) { logInfo('websocket connected to', this.url); @@ -295,6 +299,10 @@ export default class WebSocketClient { this.reconnectCallback = callback; } + public setReliableReconnectCallback(callback: () => void) { + this.reliableReconnectCallback = callback; + } + public setErrorCallback(callback: Function) { this.errorCallback = callback; } diff --git a/app/components/announcement_banner/announcement_banner.tsx b/app/components/announcement_banner/announcement_banner.tsx index 4008b5c86..08f4bcd17 100644 --- a/app/components/announcement_banner/announcement_banner.tsx +++ b/app/components/announcement_banner/announcement_banner.tsx @@ -9,6 +9,7 @@ import { View, } from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {dismissAnnouncement} from '@actions/local/systems'; import CompassIcon from '@components/compass_icon'; @@ -17,6 +18,7 @@ import {ANNOUNCEMENT_BAR_HEIGHT} from '@constants/view'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {bottomSheet} from '@screens/navigation'; +import {bottomSheetSnapPoint} from '@utils/helpers'; import {getMarkdownTextStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -82,6 +84,7 @@ const AnnouncementBanner = ({ const intl = useIntl(); const serverUrl = useServerUrl(); const height = useSharedValue(0); + const {bottom} = useSafeAreaInsets(); const theme = useTheme(); const [visible, setVisible] = useState(false); const style = getStyle(theme); @@ -100,19 +103,20 @@ const AnnouncementBanner = ({ defaultMessage: 'Announcement', }); - let snapPoint = SNAP_POINT_WITHOUT_DISMISS; - if (allowDismissal) { - snapPoint += DISMISS_BUTTON_HEIGHT; - } + const snapPoint = bottomSheetSnapPoint( + 1, + SNAP_POINT_WITHOUT_DISMISS + (allowDismissal ? DISMISS_BUTTON_HEIGHT : 0), + bottom, + ); bottomSheet({ closeButtonId: CLOSE_BUTTON_ID, title, renderContent, - snapPoints: [snapPoint, 10], + snapPoints: [1, snapPoint], theme, }); - }, [theme.sidebarHeaderTextColor, intl.locale, renderContent, allowDismissal]); + }, [theme.sidebarHeaderTextColor, intl.locale, renderContent, allowDismissal, bottom]); const handleDismiss = useCallback(() => { dismissAnnouncement(serverUrl, bannerText); diff --git a/app/components/announcement_banner/expanded_announcement_banner.tsx b/app/components/announcement_banner/expanded_announcement_banner.tsx index 22de72d02..a34938b3c 100644 --- a/app/components/announcement_banner/expanded_announcement_banner.tsx +++ b/app/components/announcement_banner/expanded_announcement_banner.tsx @@ -1,11 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {BottomSheetScrollView} from '@gorhom/bottom-sheet'; import React, {useCallback, useMemo} from 'react'; import {useIntl} from 'react-intl'; -import {Text, View} from 'react-native'; +import {ScrollView, Text, View} from 'react-native'; import Button from 'react-native-button'; -import {ScrollView} from 'react-native-gesture-handler'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {dismissAnnouncement} from '@actions/local/systems'; @@ -84,6 +84,8 @@ const ExpandedAnnouncementBanner = ({ return [style.container, {marginBottom: insets.bottom + 10}]; }, [style, insets.bottom]); + const Scroll = useMemo(() => (isTablet ? ScrollView : BottomSheetScrollView), [isTablet]); + return ( {!isTablet && ( @@ -94,7 +96,7 @@ const ExpandedAnnouncementBanner = ({ })} )} - - +