Compare commits

...

19 Commits

Author SHA1 Message Date
Mattermost Build
70119fc026 Bump app build number to 452 (#6974) (#6975)
(cherry picked from commit d417b95643)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-13 21:16:22 +02:00
Mattermost Build
0d9c6e0ad3 Fix the caret position when using the search phrase modifier (#6972) (#6973)
(cherry picked from commit 49fc180982)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-13 21:15:11 +02:00
Mattermost Build
f7d8ed9e1f Bump app build number to 451 (#6967) (#6968)
(cherry picked from commit 9411dbd669)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-12 13:07:36 +02:00
Mattermost Build
b8cc13d7fa Fix (#6960) (#6966)
(cherry picked from commit c8ded6ef3c)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
2023-01-12 13:00:51 +02:00
Mattermost Build
317568b4c8 Fix some issues found by Sentry (#6962) (#6965)
(cherry picked from commit bb351c7376)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-12 11:11:14 +02:00
Mattermost Build
55f919dd27 Add String description in log arguments (#6961) (#6964)
(cherry picked from commit 247d8371d9)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-12 11:08:12 +02:00
Mattermost Build
da1b3dc71d Add back wrongly removed Ephemeral.theme assignment (#6959) (#6963)
(cherry picked from commit cab863d62f)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-12 11:03:57 +02:00
Elias Nahum
57a9ff31bf fix schema test 2023-01-11 22:23:03 +02:00
Elias Nahum
4f86a87bdc fix ci 2023-01-11 22:14:01 +02:00
Mattermost Build
9ab21b2f62 Bump build number to 450 (#6950) (#6955)
* Fix upgrade path

* Introduce Upgrade helper

* Reset server database schema version to 1

* Enable release builds on the CI

* Bump build number to 450

(cherry picked from commit 4199b13843)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2023-01-11 21:45:47 +02:00
Mattermost Build
1934945d72 Fix code syntax highlight on full screen view (#6944) (#6954)
(cherry picked from commit 8edf128d59)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-01-11 21:45:14 +02:00
Mattermost Build
5162e6b6e7 Fix connection banner showing when not needed (#6948) (#6953)
* Fix connection banner showing when not needed

* Fix some issues and some refactoring

(cherry picked from commit 6082a6a790)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2023-01-11 21:44:31 +02:00
Guillermo Vayá
9139a26967 Merge pull request #6951 from mattermost/weblate-6947
Weblate 6947
2023-01-11 13:14:49 +01:00
Kaya Zeren
56fbb3d842 Translated using Weblate (Turkish)
Currently translated at 93.6% (879 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/tr/
2023-01-11 12:53:17 +01:00
정성근
56349f865f Translated using Weblate (Korean)
Currently translated at 59.2% (556 of 939 strings)

Translation: mattermost-languages-shipped/mattermost-mobile-v2
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/ko/
2023-01-11 12:53:01 +01:00
Elias Nahum
fdf593bcec Fix navigation theming (#6946) 2023-01-11 09:54:07 +02:00
Elias Nahum
8e2e016a6c check for analytics enabled 2023-01-09 11:14:15 +02:00
Elias Nahum
4d9bc1fbed Bump app build number to 449 (#6940) 2023-01-07 18:44:27 +02:00
Elias Nahum
7351c7ccac fix Sentry import 2023-01-07 18:39:26 +02:00
35 changed files with 508 additions and 382 deletions

View File

@@ -309,13 +309,13 @@ jobs:
- save: - save:
filename: "*.apk" filename: "*.apk"
# build-android-release: build-android-release:
# executor: android executor: android
# steps: steps:
# - build-android - build-android
# - persist - persist
# - save: - save:
# filename: "*.apk" filename: "*.apk"
build-android-pr: build-android-pr:
executor: android executor: android
@@ -326,27 +326,27 @@ jobs:
- save: - save:
filename: "*.apk" filename: "*.apk"
# build-android-unsigned: build-android-unsigned:
# executor: android executor: android
# steps: steps:
# - checkout: - checkout:
# path: ~/mattermost-mobile path: ~/mattermost-mobile
# - npm-dependencies - npm-dependencies
# - assets - assets
# - fastlane-dependencies: - fastlane-dependencies:
# for: android for: android
# - gradle-dependencies - gradle-dependencies
# - run: - run:
# name: Jetify Android libraries name: Jetify Android libraries
# command: ./node_modules/.bin/jetify command: ./node_modules/.bin/jetify
# - run: - run:
# working_directory: fastlane working_directory: fastlane
# name: Run fastlane to build unsigned android name: Run fastlane to build unsigned android
# no_output_timeout: 30m no_output_timeout: 30m
# command: bundle exec fastlane android unsigned command: bundle exec fastlane android unsigned
# - persist - persist
# - save: - save:
# filename: "*.apk" filename: "*.apk"
build-ios-beta: build-ios-beta:
executor: executor:
@@ -358,13 +358,13 @@ jobs:
- save: - save:
filename: "*.ipa" filename: "*.ipa"
# build-ios-release: build-ios-release:
# executor: ios executor: ios
# steps: steps:
# - build-ios - build-ios
# - persist - persist
# - save: - save:
# filename: "*.ipa" filename: "*.ipa"
build-ios-pr: build-ios-pr:
executor: ios executor: ios
@@ -375,63 +375,64 @@ jobs:
- save: - save:
filename: "*.ipa" filename: "*.ipa"
# build-ios-unsigned: build-ios-unsigned:
# executor: ios executor: ios
# steps: steps:
# - checkout: - checkout:
# path: ~/mattermost-mobile path: ~/mattermost-mobile
# - npm-dependencies - npm-dependencies
# - pods-dependencies - pods-dependencies
# - assets - assets
# - fastlane-dependencies: - fastlane-dependencies:
# for: ios for: ios
# - run: - run:
# working_directory: fastlane working_directory: fastlane
# name: Run fastlane to build unsigned iOS name: Run fastlane to build unsigned iOS
# no_output_timeout: 30m no_output_timeout: 30m
# command: | command: |
# HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
# bundle exec fastlane ios unsigned bundle exec fastlane ios unsigned
# - persist_to_workspace: - persist_to_workspace:
# root: ~/ root: ~/
# paths: paths:
# - mattermost-mobile/*.ipa - mattermost-mobile/*.ipa
# - save: - save:
# filename: "*.ipa" filename: "*.ipa"
# build-ios-simulator: build-ios-simulator:
# executor: ios executor: ios
# steps: steps:
# - checkout: - checkout:
# path: ~/mattermost-mobile path: ~/mattermost-mobile
# - npm-dependencies - npm-dependencies
# - pods-dependencies - pods-dependencies
# - assets - assets
# - fastlane-dependencies: - fastlane-dependencies:
# for: ios for: ios
# - run: - run:
# working_directory: fastlane working_directory: fastlane
# name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator
# no_output_timeout: 30m no_output_timeout: 30m
# command: | command: |
# HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
# bundle exec fastlane ios simulator bundle exec fastlane ios simulator
# - persist_to_workspace: - persist_to_workspace:
# root: ~/ root: ~/
# paths: paths:
# - mattermost-mobile/Mattermost-simulator-x86_64.app.zip - mattermost-mobile/Mattermost-simulator-x86_64.app.zip
# - save: - save:
# filename: "Mattermost-simulator-x86_64.app.zip" filename: "Mattermost-simulator-x86_64.app.zip"
# deploy-android-release: deploy-android-release:
# executor: executor:
# name: android name: android
# resource_class: medium resource_class: medium
# steps: steps:
# - deploy-to-store: - deploy-to-store:
# task: "Deploy to Google Play" task: "Deploy to Google Play"
# target: android target: android
# file: "*.apk" file: "*.apk"
env: "SUPPLY_TRACK=beta"
deploy-android-beta: deploy-android-beta:
executor: executor:
@@ -444,13 +445,14 @@ jobs:
file: "*.apk" file: "*.apk"
env: "SUPPLY_TRACK=alpha" env: "SUPPLY_TRACK=alpha"
# deploy-ios-release: deploy-ios-release:
# executor: ios executor: ios
# steps: steps:
# - deploy-to-store: - deploy-to-store:
# task: "Deploy to TestFlight" task: "Deploy to TestFlight"
# target: ios target: ios
# file: "*.ipa" file: "*.ipa"
env: ""
deploy-ios-beta: deploy-ios-beta:
executor: ios executor: ios
@@ -461,17 +463,17 @@ jobs:
file: "*.ipa" file: "*.ipa"
env: "" env: ""
# github-release: github-release:
# executor: executor:
# name: android name: android
# resource_class: medium resource_class: medium
# steps: steps:
# - attach_workspace: - attach_workspace:
# at: ~/ at: ~/
# - run: - run:
# name: Create GitHub release name: Create GitHub release
# working_directory: fastlane working_directory: fastlane
# command: bundle exec fastlane github command: bundle exec fastlane github
workflows: workflows:
version: 2 version: 2
@@ -483,26 +485,24 @@ workflows:
# requires: # requires:
# - test # - test
# - build-android-release: - build-android-release:
# context: mattermost-mobile-android-release context: mattermost-mobile-android-release
# requires: requires:
# - test - test
# filters: filters:
# branches: branches:
# only: only:
# - /^build-\d+$/ - /^build-release-\d+$/
# - /^build-android-\d+$/ - /^build-android-release-\d+$/
# - /^build-android-release-\d+$/ - deploy-android-release:
# - deploy-android-release: context: mattermost-mobile-android-release
# context: mattermost-mobile-android-release requires:
# requires: - build-android-release
# - build-android-release filters:
# filters: branches:
# branches: only:
# only: - /^build-release-\d+$/
# - /^build-\d+$/ - /^build-android-release-\d+$/
# - /^build-android-\d+$/
# - /^build-android-release-\d+$/
- build-android-beta: - build-android-beta:
context: mattermost-mobile-android-beta context: mattermost-mobile-android-beta
@@ -523,26 +523,24 @@ workflows:
- /^build-android-\d+$/ - /^build-android-\d+$/
- /^build-android-beta-\d+$/ - /^build-android-beta-\d+$/
# - build-ios-release: - build-ios-release:
# context: mattermost-mobile-ios-release context: mattermost-mobile-ios-release
# requires: requires:
# - test - test
# filters: filters:
# branches: branches:
# only: only:
# - /^build-\d+$/ - /^build-release-\d+$/
# - /^build-ios-\d+$/ - /^build-ios-release-\d+$/
# - /^build-ios-release-\d+$/ - deploy-ios-release:
# - deploy-ios-release: context: mattermost-mobile-ios-release
# context: mattermost-mobile-ios-release requires:
# requires: - build-ios-release
# - build-ios-release filters:
# filters: branches:
# branches: only:
# only: - /^build-release-\d+$/
# - /^build-\d+$/ - /^build-ios-release-\d+$/
# - /^build-ios-\d+$/
# - /^build-ios-release-\d+$/
- build-ios-beta: - build-ios-beta:
context: mattermost-mobile-ios-beta context: mattermost-mobile-ios-beta
@@ -578,43 +576,41 @@ workflows:
branches: branches:
only: /^(build|ios)-pr-.*/ only: /^(build|ios)-pr-.*/
# - build-android-unsigned: - build-android-unsigned:
# context: mattermost-mobile-unsigned context: mattermost-mobile-unsigned
# requires: requires:
# - test - test
# filters: filters:
# tags: tags:
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
# branches: branches:
# only: unsigned only: unsigned
# - build-ios-unsigned: - build-ios-unsigned:
# context: mattermost-mobile-unsigned context: mattermost-mobile-unsigned
# requires: requires:
# - test - test
# filters: filters:
# tags: tags:
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
# branches: branches:
# only: unsigned only: unsigned
# - build-ios-simulator: - build-ios-simulator:
# context: mattermost-mobile-unsigned context: mattermost-mobile-unsigned
# requires: requires:
# - test - test
# filters: filters:
# branches: branches:
# only: only:
# - /^build-\d+$/ - /^build-\d+$/
# - /^build-ios-\d+$/ - /^build-ios-sim-\d+$/
# - /^build-ios-beta-\d+$/
# - /^build-ios-sim-\d+$/
# - github-release: - github-release:
# context: mattermost-mobile-unsigned context: mattermost-mobile-unsigned
# requires: requires:
# - build-android-unsigned - build-android-unsigned
# - build-ios-unsigned - build-ios-unsigned
# filters: filters:
# tags: tags:
# only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/ only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
# branches: branches:
# only: unsigned only: unsigned

View File

@@ -145,7 +145,7 @@ android {
applicationId "com.mattermost.rnbeta" applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 448 versionCode 452
versionName "2.0.0" versionName "2.0.0"
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -8,6 +8,7 @@ import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnecte
import {getCurrentUser} from '@queries/servers/user'; import {getCurrentUser} from '@queries/servers/user';
import {setTeamLoading} from '@store/team_load_store'; import {setTeamLoading} from '@store/team_load_store';
import {deleteV1Data} from '@utils/file'; import {deleteV1Data} from '@utils/file';
import {isTablet} from '@utils/helpers';
import {logInfo} from '@utils/log'; import {logInfo} from '@utils/log';
import {handleEntryAfterLoadNavigation, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common'; import {handleEntryAfterLoadNavigation, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
@@ -29,7 +30,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
// clear lastUnreadChannelId // clear lastUnreadChannelId
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''}); const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
if (removeLastUnreadChannelId) { if (removeLastUnreadChannelId) {
operator.batchRecords(removeLastUnreadChannelId); await operator.batchRecords(removeLastUnreadChannelId);
} }
const {database} = operator; 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; const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData;
if (isUpgrade && meData?.user) { 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) { if (me?.length) {
await operator.batchRecords(me); await operator.batchRecords(me);
} }
@@ -84,8 +90,8 @@ export async function upgradeEntry(serverUrl: string) {
const error = configAndLicense.error || entryData.error; const error = configAndLicense.error || entryData.error;
if (!error) { if (!error) {
DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId); await DatabaseManager.updateServerIdentifier(serverUrl, configAndLicense.config!.DiagnosticId);
DatabaseManager.setActiveServerDatabase(serverUrl); await DatabaseManager.setActiveServerDatabase(serverUrl);
deleteV1Data(); deleteV1Data();
} }

View File

@@ -544,9 +544,15 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn
} }
if (promises.length) { if (promises.length) {
const result = await Promise.all(promises); const authorsResult = await Promise.allSettled(promises);
const authors = result.flat(); const result = authorsResult.reduce<UserProfile[][]>((acc, item) => {
if (item.status === 'fulfilled') {
acc.push(item.value);
}
return acc;
}, []);
const authors = result.flat();
if (!fetchOnly && authors.length) { if (!fetchOnly && authors.length) {
await operator.handleUsers({ await operator.handleUsers({
users: authors, users: authors,

View File

@@ -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({ const postModels = await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_NEW, actionType,
order: [post.id], order: [post.id],
posts: [post], posts: [post],
prepareRecordsOnly: true, prepareRecordsOnly: true,
@@ -203,8 +208,14 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage)
models.push(...authorsModels); 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({ const postModels = await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_NEW, actionType,
order: [post.id], order: [post.id],
posts: [post], posts: [post],
prepareRecordsOnly: true, prepareRecordsOnly: true,

View File

@@ -2,12 +2,22 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {markTeamThreadsAsRead, processReceivedThreads, updateThread} from '@actions/local/thread'; 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'; import EphemeralStore from '@store/ephemeral_store';
export async function handleThreadUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> { export async function handleThreadUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
try { try {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return;
}
const thread: Thread = JSON.parse(msg.data.thread); 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 // Mark it as following
thread.is_following = true; thread.is_following = true;

View File

@@ -33,6 +33,7 @@ export default class WebSocketClient {
private firstConnectCallback?: () => void; private firstConnectCallback?: () => void;
private missedEventsCallback?: () => void; private missedEventsCallback?: () => void;
private reconnectCallback?: () => void; private reconnectCallback?: () => void;
private reliableReconnectCallback?: () => void;
private errorCallback?: Function; private errorCallback?: Function;
private closeCallback?: (connectFailCount: number, lastDisconnect: number) => void; private closeCallback?: (connectFailCount: number, lastDisconnect: number) => void;
private connectingCallback?: () => void; private connectingCallback?: () => void;
@@ -148,8 +149,11 @@ export default class WebSocketClient {
logInfo('websocket re-established connection to', this.url); logInfo('websocket re-established connection to', this.url);
if (!reliableWebSockets && this.reconnectCallback) { if (!reliableWebSockets && this.reconnectCallback) {
this.reconnectCallback(); this.reconnectCallback();
} else if (reliableWebSockets && this.serverSequence && this.missedEventsCallback) { } else if (reliableWebSockets) {
this.missedEventsCallback(); this.reliableReconnectCallback?.();
if (this.serverSequence && this.missedEventsCallback) {
this.missedEventsCallback();
}
} }
} else if (this.firstConnectCallback) { } else if (this.firstConnectCallback) {
logInfo('websocket connected to', this.url); logInfo('websocket connected to', this.url);
@@ -295,6 +299,10 @@ export default class WebSocketClient {
this.reconnectCallback = callback; this.reconnectCallback = callback;
} }
public setReliableReconnectCallback(callback: () => void) {
this.reliableReconnectCallback = callback;
}
public setErrorCallback(callback: Function) { public setErrorCallback(callback: Function) {
this.errorCallback = callback; this.errorCallback = callback;
} }

View File

@@ -106,6 +106,7 @@ const ConnectionBanner = ({
} }
return () => { return () => {
clearTimeoutRef(openTimeout); clearTimeoutRef(openTimeout);
clearTimeoutRef(closeTimeout);
}; };
}, []); }, []);
@@ -125,7 +126,7 @@ const ConnectionBanner = ({
} }
}, [isConnected]); }, [isConnected]);
useEffect(() => { useDidUpdate(() => {
if (appState === 'active') { if (appState === 'active') {
if (!isConnected && !visible) { if (!isConnected && !visible) {
if (!openTimeout.current) { if (!openTimeout.current) {
@@ -138,10 +139,11 @@ const ConnectionBanner = ({
} }
} }
} else { } else {
setVisible(false);
clearTimeoutRef(openTimeout); clearTimeoutRef(openTimeout);
clearTimeoutRef(closeTimeout); clearTimeoutRef(closeTimeout);
} }
}, [appState]); }, [appState === 'active']);
useEffect(() => { useEffect(() => {
height.value = withTiming(visible ? ANNOUNCEMENT_BAR_HEIGHT : 0, { height.value = withTiming(visible ? ANNOUNCEMENT_BAR_HEIGHT : 0, {
@@ -149,12 +151,6 @@ const ConnectionBanner = ({
}); });
}, [visible]); }, [visible]);
useEffect(() => {
return () => {
clearTimeoutRef(closeTimeout);
};
});
const bannerStyle = useAnimatedStyle(() => ({ const bannerStyle = useAnimatedStyle(() => ({
height: height.value, height: height.value,
})); }));

View File

@@ -85,7 +85,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc
const screen = Screens.CODE; const screen = Screens.CODE;
const passProps = { const passProps = {
code: content, code: content,
language, language: getHighlightLanguageFromNameOrAlias(language),
textStyle, textStyle,
}; };

View File

@@ -142,20 +142,23 @@ export const MODAL_SCREENS_WITHOUT_BACK = new Set<string>([
EDIT_POST, EDIT_POST,
EDIT_PROFILE, EDIT_PROFILE,
EDIT_SERVER, EDIT_SERVER,
EMOJI_PICKER,
FIND_CHANNELS, FIND_CHANNELS,
GALLERY, GALLERY,
PERMALINK, PERMALINK,
REACTIONS,
]); ]);
export const SCREENS_WITH_TRANSPARENT_BACKGROUND = new Set<string>([ export const SCREENS_WITH_TRANSPARENT_BACKGROUND = new Set<string>([
PERMALINK,
REVIEW_APP,
SNACK_BAR,
]);
export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
BOTTOM_SHEET, BOTTOM_SHEET,
EMOJI_PICKER,
POST_OPTIONS, POST_OPTIONS,
THREAD_OPTIONS, THREAD_OPTIONS,
PERMALINK,
REACTIONS, REACTIONS,
SNACK_BAR,
USER_PROFILE, USER_PROFILE,
]); ]);

View File

@@ -22,6 +22,7 @@ import AppDataOperator from '@database/operator/app_data_operator';
import ServerDataOperator from '@database/operator/server_data_operator'; import ServerDataOperator from '@database/operator/server_data_operator';
import {schema as appSchema} from '@database/schema/app'; import {schema as appSchema} from '@database/schema/app';
import {serverSchema} from '@database/schema/server'; import {serverSchema} from '@database/schema/server';
import {beforeUpgrade} from '@helpers/database/upgrade';
import {getActiveServer, getServer, getServerByIdentifier} from '@queries/app/servers'; import {getActiveServer, getServer, getServerByIdentifier} from '@queries/app/servers';
import {querySystemValue} from '@queries/servers/system'; import {querySystemValue} from '@queries/servers/system';
import {deleteLegacyFileCache} from '@utils/file'; import {deleteLegacyFileCache} from '@utils/file';
@@ -63,14 +64,17 @@ class DatabaseManager {
*/ */
public init = async (serverUrls: string[]): Promise<void> => { public init = async (serverUrls: string[]): Promise<void> => {
await this.createAppDatabase(); await this.createAppDatabase();
const buildNumber = DeviceInfo.getBuildNumber();
const versionNumber = DeviceInfo.getVersion();
await beforeUpgrade.call(this, serverUrls, versionNumber, buildNumber);
for await (const serverUrl of serverUrls) { for await (const serverUrl of serverUrls) {
await this.initServerDatabase(serverUrl); await this.initServerDatabase(serverUrl);
} }
this.appDatabase?.operator.handleInfo({ this.appDatabase?.operator.handleInfo({
info: [{ info: [{
build_number: DeviceInfo.getBuildNumber(), build_number: buildNumber,
created_at: Date.now(), created_at: Date.now(),
version_number: DeviceInfo.getVersion(), version_number: versionNumber,
}], }],
prepareRecordsOnly: false, prepareRecordsOnly: false,
}); });
@@ -428,7 +432,7 @@ class DatabaseManager {
*/ */
private deleteServerDatabaseFiles = async (serverUrl: string): Promise<void> => { private deleteServerDatabaseFiles = async (serverUrl: string): Promise<void> => {
const databaseName = urlSafeBase64Encode(serverUrl); const databaseName = urlSafeBase64Encode(serverUrl);
this.deleteServerDatabaseFilesByName(databaseName); return this.deleteServerDatabaseFilesByName(databaseName);
}; };
/** /**
@@ -439,7 +443,7 @@ class DatabaseManager {
private deleteServerDatabaseFilesByName = async (databaseName: string): Promise<void> => { private deleteServerDatabaseFilesByName = async (databaseName: string): Promise<void> => {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
// On iOS, we'll delete the *.db file under the shared app-group/databases folder // On iOS, we'll delete the *.db file under the shared app-group/databases folder
deleteIOSDatabase({databaseName}); await deleteIOSDatabase({databaseName});
return; return;
} }
@@ -449,14 +453,15 @@ class DatabaseManager {
const databaseShm = `${androidFilesDir}${databaseName}.db-shm`; const databaseShm = `${androidFilesDir}${databaseName}.db-shm`;
const databaseWal = `${androidFilesDir}${databaseName}.db-wal`; const databaseWal = `${androidFilesDir}${databaseName}.db-wal`;
FileSystem.unlink(databaseFile).catch(emptyFunction); await FileSystem.unlink(databaseFile).catch(emptyFunction);
FileSystem.unlink(databaseShm).catch(emptyFunction); await FileSystem.unlink(databaseShm).catch(emptyFunction);
FileSystem.unlink(databaseWal).catch(emptyFunction); await FileSystem.unlink(databaseWal).catch(emptyFunction);
}; };
/** /**
* deleteServerDatabaseFilesByName: Removes the *.db file from the App-Group directory for iOS or the files directory for Android, given the database name * renameDatabase: Renames the *.db file from the App-Group directory for iOS or the files directory for Android
* @param {string} databaseName * @param {string} databaseName
* @param {string} newDBName
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
private renameDatabase = async (databaseName: string, newDBName: string): Promise<void> => { private renameDatabase = async (databaseName: string, newDBName: string): Promise<void> => {

View File

@@ -4,105 +4,6 @@
// NOTE : To implement migration, please follow this document // NOTE : To implement migration, please follow this document
// https://nozbe.github.io/WatermelonDB/Advanced/Migrations.html // https://nozbe.github.io/WatermelonDB/Advanced/Migrations.html
import {schemaMigrations, addColumns, createTable} from '@nozbe/watermelondb/Schema/migrations'; import {schemaMigrations} from '@nozbe/watermelondb/Schema/migrations';
import {MM_TABLES} from '@constants/database'; export default schemaMigrations({migrations: []});
import {tableSchemaSpec as configSpec} from '@database/schema/server/table_schemas/config';
import {tableSchemaSpec as teamThreadsSyncSpec} from '@database/schema/server/table_schemas/team_threads_sync';
import {tableSchemaSpec as threadSpec} from '@database/schema/server/table_schemas/thread';
import {tableSchemaSpec as threadInTeamSpec} from '@database/schema/server/table_schemas/thread_in_team';
import {tableSchemaSpec as threadParticipantSpec} from '@database/schema/server/table_schemas/thread_participant';
const {SERVER: {
GROUP,
MY_CHANNEL,
TEAM,
THREAD,
THREAD_PARTICIPANT,
THREADS_IN_TEAM,
USER,
}} = MM_TABLES;
export default schemaMigrations({migrations: [
{
toVersion: 7,
steps: [
// Along with adding the new table - TeamThreadsSync,
// We need to clear the data in thread related tables (DROP & CREATE) to fetch the fresh data from the server
createTable({
...teamThreadsSyncSpec,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
unsafeSql: (baseSql) => {
return `
${baseSql}
DROP TABLE ${THREAD};
DROP TABLE ${THREADS_IN_TEAM};
DROP TABLE ${THREAD_PARTICIPANT};
`;
},
}),
createTable(threadSpec),
createTable(threadInTeamSpec),
createTable(threadParticipantSpec),
],
},
{
toVersion: 6,
steps: [
addColumns({
table: USER,
columns: [
{name: 'terms_of_service_id', type: 'string'},
{name: 'terms_of_service_create_at', type: 'number'},
],
}),
],
},
{
toVersion: 5,
steps: [
createTable(configSpec),
],
},
{
toVersion: 4,
steps: [
addColumns({
table: TEAM,
columns: [
{name: 'invite_id', type: 'string'},
],
}),
],
},
{
toVersion: 3,
steps: [
addColumns({
table: GROUP,
columns: [
{name: 'member_count', type: 'number'},
],
}),
],
},
{
toVersion: 2,
steps: [
addColumns({
table: MY_CHANNEL,
columns: [
{name: 'last_fetched_at', type: 'number', isIndexed: true},
],
}),
addColumns({
table: THREAD,
columns: [
{name: 'last_fetched_at', type: 'number', isIndexed: true},
],
}),
],
},
]});

View File

@@ -39,7 +39,7 @@ import {
} from './table_schemas'; } from './table_schemas';
export const serverSchema: AppSchema = appSchema({ export const serverSchema: AppSchema = appSchema({
version: 7, version: 1,
tables: [ tables: [
CategorySchema, CategorySchema,
CategoryChannelSchema, CategoryChannelSchema,

View File

@@ -45,7 +45,7 @@ const {
describe('*** Test schema for SERVER database ***', () => { describe('*** Test schema for SERVER database ***', () => {
it('=> The SERVER SCHEMA should strictly match', () => { it('=> The SERVER SCHEMA should strictly match', () => {
expect(serverSchema).toEqual({ expect(serverSchema).toEqual({
version: 7, version: 1,
unsafeSql: undefined, unsafeSql: undefined,
tables: { tables: {
[CATEGORY]: { [CATEGORY]: {

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getLastInstalledVersion} from '@queries/app/info';
import {logError, logInfo} from '@utils/log';
import type {DatabaseManager} from '@typings/database/manager';
import type InfoModel from '@typings/database/models/app/info';
export async function beforeUpgrade(serverUrls: string[], versionNumber: string, buildNumber: string) {
const info = await getLastInstalledVersion();
const manager: DatabaseManager | undefined = this.serverDatabases ? this : undefined;
if (manager && serverUrls.length && info && (versionNumber !== info.versionNumber || buildNumber !== info.buildNumber)) {
await beforeUpgradeTo450(manager, serverUrls, info);
}
}
async function beforeUpgradeTo450(manager: DatabaseManager, serverUrls: string[], info: InfoModel) {
try {
const buildNumber = parseInt(info.buildNumber, 10);
if (info.versionNumber === '2.0.0' && buildNumber < 450) {
for await (const serverUrl of serverUrls) {
logInfo('Remove database before upgrading for', serverUrl);
await manager.deleteServerDatabaseFiles(serverUrl);
}
}
} catch (e) {
logError('Error running the upgrade before build 450', e);
}
}

View File

@@ -47,11 +47,11 @@ export async function initialize() {
export async function start() { export async function start() {
await initialize(); await initialize();
await WebsocketManager.init(serverCredentials);
PushNotifications.init(serverCredentials.length > 0); PushNotifications.init(serverCredentials.length > 0);
registerNavigationListeners(); registerNavigationListeners();
registerScreens(); registerScreens();
initialLaunch(); await initialLaunch();
WebsocketManager.init(serverCredentials);
} }

View File

@@ -27,7 +27,7 @@ const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, Pu
export const initialLaunch = async () => { export const initialLaunch = async () => {
const deepLinkUrl = await Linking.getInitialURL(); const deepLinkUrl = await Linking.getInitialURL();
if (deepLinkUrl) { if (deepLinkUrl) {
launchAppFromDeepLink(deepLinkUrl, true); await launchAppFromDeepLink(deepLinkUrl, true);
return; return;
} }
@@ -43,21 +43,21 @@ export const initialLaunch = async () => {
tapped = delivered.find((d) => (d as unknown as NotificationData).ack_id === notification?.payload.ack_id) == null; tapped = delivered.find((d) => (d as unknown as NotificationData).ack_id === notification?.payload.ack_id) == null;
} }
if (initialNotificationTypes.includes(notification?.payload?.type) && tapped) { if (initialNotificationTypes.includes(notification?.payload?.type) && tapped) {
launchAppFromNotification(convertToNotificationData(notification!), true); await launchAppFromNotification(convertToNotificationData(notification!), true);
return; return;
} }
launchApp({launchType: Launch.Normal, coldStart: true}); await launchApp({launchType: Launch.Normal, coldStart: true});
}; };
const launchAppFromDeepLink = (deepLinkUrl: string, coldStart = false) => { const launchAppFromDeepLink = async (deepLinkUrl: string, coldStart = false) => {
const props = getLaunchPropsFromDeepLink(deepLinkUrl, coldStart); const props = getLaunchPropsFromDeepLink(deepLinkUrl, coldStart);
launchApp(props); return launchApp(props);
}; };
const launchAppFromNotification = async (notification: NotificationWithData, coldStart = false) => { const launchAppFromNotification = async (notification: NotificationWithData, coldStart = false) => {
const props = await getLaunchPropsFromNotification(notification, coldStart); const props = await getLaunchPropsFromNotification(notification, coldStart);
launchApp(props); return launchApp(props);
}; };
/** /**

View File

@@ -28,7 +28,9 @@ export class Analytics {
}; };
async init(config: ClientConfig) { async init(config: ClientConfig) {
this.analytics = require('@rudderstack/rudder-sdk-react-native').default; if (LocalConfig.RudderApiKey) {
this.analytics = require('@rudderstack/rudder-sdk-react-native').default;
}
if (this.analytics) { if (this.analytics) {
const {height, width} = Dimensions.get('window'); const {height, width} = Dimensions.get('window');
@@ -123,10 +125,18 @@ export class Analytics {
} }
trackAPI(event: string, props?: any) { trackAPI(event: string, props?: any) {
if (!this.analytics) {
return;
}
this.trackEvent('api', event, props); this.trackEvent('api', event, props);
} }
trackCommand(event: string, command: string, errorMessage?: string) { trackCommand(event: string, command: string, errorMessage?: string) {
if (!this.analytics) {
return;
}
const sanitizedCommand = this.sanitizeCommand(command); const sanitizedCommand = this.sanitizeCommand(command);
let props: any; let props: any;
if (errorMessage) { if (errorMessage) {
@@ -139,6 +149,9 @@ export class Analytics {
} }
trackAction(event: string, props?: any) { trackAction(event: string, props?: any) {
if (!this.analytics) {
return;
}
this.trackEvent('action', event, props); this.trackEvent('action', event, props);
} }

View File

@@ -78,8 +78,9 @@ class SessionManager {
}; };
private clearCookiesForServer = async (serverUrl: string) => { private clearCookiesForServer = async (serverUrl: string) => {
this.clearCookies(serverUrl, false);
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
this.clearCookies(serverUrl, false);
// Also delete any cookies that were set by react-native-webview // Also delete any cookies that were set by react-native-webview
this.clearCookies(serverUrl, true); this.clearCookies(serverUrl, true);
} else if (Platform.OS === 'android') { } else if (Platform.OS === 'android') {

View File

@@ -30,12 +30,12 @@ class WebsocketManager {
private connectionTimerIDs: Record<string, DebouncedFunc<() => void>> = {}; private connectionTimerIDs: Record<string, DebouncedFunc<() => void>> = {};
private isBackgroundTimerRunning = false; private isBackgroundTimerRunning = false;
private netConnected = false; private netConnected = false;
private previousAppState: AppStateStatus; private previousActiveState: boolean;
private statusUpdatesIntervalIDs: Record<string, NodeJS.Timer> = {}; private statusUpdatesIntervalIDs: Record<string, NodeJS.Timer> = {};
private backgroundIntervalId: number | undefined; private backgroundIntervalId: number | undefined;
constructor() { constructor() {
this.previousAppState = AppState.currentState; this.previousActiveState = AppState.currentState === 'active';
} }
public init = async (serverCredentials: ServerCredential[]) => { public init = async (serverCredentials: ServerCredential[]) => {
@@ -81,6 +81,7 @@ class WebsocketManager {
//client.setMissedEventsCallback(() => {}) Nothing to do on missedEvents callback //client.setMissedEventsCallback(() => {}) Nothing to do on missedEvents callback
client.setReconnectCallback(() => this.onReconnect(serverUrl)); client.setReconnectCallback(() => this.onReconnect(serverUrl));
client.setReliableReconnectCallback(() => this.onReliableReconnect(serverUrl));
client.setCloseCallback((connectFailCount: number, lastDisconnect: number) => this.onWebsocketClose(serverUrl, connectFailCount, lastDisconnect)); client.setCloseCallback((connectFailCount: number, lastDisconnect: number) => this.onWebsocketClose(serverUrl, connectFailCount, lastDisconnect));
if (this.netConnected && ['unknown', 'active'].includes(AppState.currentState)) { if (this.netConnected && ['unknown', 'active'].includes(AppState.currentState)) {
@@ -153,23 +154,27 @@ class WebsocketManager {
private onFirstConnect = (serverUrl: string) => { private onFirstConnect = (serverUrl: string) => {
this.startPeriodicStatusUpdates(serverUrl); this.startPeriodicStatusUpdates(serverUrl);
handleFirstConnect(serverUrl);
this.getConnectedSubject(serverUrl).next('connected'); this.getConnectedSubject(serverUrl).next('connected');
handleFirstConnect(serverUrl);
}; };
private onReconnect = async (serverUrl: string) => { private onReconnect = async (serverUrl: string) => {
this.startPeriodicStatusUpdates(serverUrl); this.startPeriodicStatusUpdates(serverUrl);
this.getConnectedSubject(serverUrl).next('connected');
await handleReconnect(serverUrl); await handleReconnect(serverUrl);
};
private onReliableReconnect = async (serverUrl: string) => {
this.getConnectedSubject(serverUrl).next('connected'); this.getConnectedSubject(serverUrl).next('connected');
}; };
private onWebsocketClose = async (serverUrl: string, connectFailCount: number, lastDisconnect: number) => { private onWebsocketClose = async (serverUrl: string, connectFailCount: number, lastDisconnect: number) => {
this.getConnectedSubject(serverUrl).next('not_connected');
if (connectFailCount <= 1) { // First fail if (connectFailCount <= 1) { // First fail
await setCurrentUserStatusOffline(serverUrl); await setCurrentUserStatusOffline(serverUrl);
await handleClose(serverUrl, lastDisconnect); await handleClose(serverUrl, lastDisconnect);
this.stopPeriodicStatusUpdates(serverUrl); this.stopPeriodicStatusUpdates(serverUrl);
this.getConnectedSubject(serverUrl).next('not_connected');
} }
}; };
@@ -205,14 +210,15 @@ class WebsocketManager {
} }
private onAppStateChange = async (appState: AppStateStatus) => { private onAppStateChange = async (appState: AppStateStatus) => {
if (appState === this.previousAppState) { const isActive = appState === 'active';
if (isActive === this.previousActiveState) {
return; return;
} }
const isMain = isMainActivity(); const isMain = isMainActivity();
this.cancelAllConnections(); this.cancelAllConnections();
if (appState !== 'active' && !this.isBackgroundTimerRunning) { if (!isActive && !this.isBackgroundTimerRunning) {
this.isBackgroundTimerRunning = true; this.isBackgroundTimerRunning = true;
this.cancelAllConnections(); this.cancelAllConnections();
this.backgroundIntervalId = BackgroundTimer.setInterval(() => { this.backgroundIntervalId = BackgroundTimer.setInterval(() => {
@@ -221,22 +227,22 @@ class WebsocketManager {
this.isBackgroundTimerRunning = false; this.isBackgroundTimerRunning = false;
}, WAIT_TO_CLOSE); }, WAIT_TO_CLOSE);
this.previousAppState = appState; this.previousActiveState = isActive;
return; return;
} }
if (appState === 'active' && this.netConnected && isMain) { // Reopen the websockets only if there is connection if (isActive && this.netConnected && isMain) { // Reopen the websockets only if there is connection
if (this.backgroundIntervalId) { if (this.backgroundIntervalId) {
BackgroundTimer.clearInterval(this.backgroundIntervalId); BackgroundTimer.clearInterval(this.backgroundIntervalId);
} }
this.isBackgroundTimerRunning = false; this.isBackgroundTimerRunning = false;
this.openAll(); this.openAll();
this.previousAppState = appState; this.previousActiveState = isActive;
return; return;
} }
if (isMain) { if (isMain) {
this.previousAppState = appState; this.previousActiveState = isActive;
} }
}; };
@@ -248,7 +254,7 @@ class WebsocketManager {
this.netConnected = newState; this.netConnected = newState;
if (this.netConnected && this.previousAppState === 'active') { // Reopen the websockets only if the app is active if (this.netConnected && this.previousActiveState) { // Reopen the websockets only if the app is active
this.openAll(); this.openAll();
return; return;
} }

24
app/queries/app/info.ts Normal file
View File

@@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import type InfoModel from '@typings/database/models/app/info';
const {APP: {INFO}} = MM_TABLES;
export const getLastInstalledVersion = async () => {
try {
const {database} = DatabaseManager.getAppDatabaseAndOperator();
const infos = await database.get<InfoModel>(INFO).query(
Q.sortBy('created_at', Q.desc),
Q.take(1),
).fetch();
return infos[0];
} catch {
return undefined;
}
};

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {Dispatch, RefObject, SetStateAction, useCallback} from 'react'; import React, {Dispatch, RefObject, SetStateAction, useCallback} from 'react';
import {StyleSheet} from 'react-native'; import {Platform, StyleSheet} from 'react-native';
import OptionItem from '@components/option_item'; import OptionItem from '@components/option_item';
import {SearchRef} from '@components/search'; import {SearchRef} from '@components/search';
@@ -33,6 +33,12 @@ const Modifier = ({item, searchRef, searchValue, setSearchValue}: Props) => {
addModifierTerm(item.term); addModifierTerm(item.term);
}, [item.term, searchValue]); }, [item.term, searchValue]);
const setNativeCursorPositionProp = (position?: number) => {
setTimeout(() => {
searchRef.current?.setNativeProps({selection: {start: position, end: position}});
}, 50);
};
const addModifierTerm = preventDoubleTap((modifierTerm) => { const addModifierTerm = preventDoubleTap((modifierTerm) => {
let newValue = ''; let newValue = '';
if (!searchValue) { if (!searchValue) {
@@ -46,9 +52,16 @@ const Modifier = ({item, searchRef, searchValue, setSearchValue}: Props) => {
setSearchValue(newValue); setSearchValue(newValue);
if (item.cursorPosition) { if (item.cursorPosition) {
const position = newValue.length + item.cursorPosition; const position = newValue.length + item.cursorPosition;
setTimeout(() => { setNativeCursorPositionProp(position);
searchRef.current?.setNativeProps({selection: {start: position, end: position}});
}, 50); if (Platform.OS === 'android') {
// on Android the selection set by setNativeProps is permanent thus the caret returns to the same
// position after we stop typing for a few ms. By setting the position to undefined,
// then the caret remains in place.
setTimeout(() => {
setNativeCursorPositionProp(undefined);
}, 50);
}
} }
}); });

View File

@@ -219,11 +219,7 @@ Appearance.addChangeListener(() => {
}); });
export function getThemeFromState(): Theme { export function getThemeFromState(): Theme {
if (EphemeralStore.theme) { return EphemeralStore.theme || getDefaultThemeByAppearance();
return EphemeralStore.theme;
}
return getDefaultThemeByAppearance();
} }
// This is a temporary helper function to avoid // This is a temporary helper function to avoid
@@ -479,12 +475,17 @@ export function goToScreen(name: string, title: string, passProps = {}, options
}); });
} }
export function popTopScreen(screenId?: string) { export async function popTopScreen(screenId?: string) {
if (screenId) { try {
Navigation.pop(screenId); if (screenId) {
} else { await Navigation.pop(screenId);
const componentId = NavigationStore.getVisibleScreen(); } else {
Navigation.pop(componentId); const componentId = NavigationStore.getVisibleScreen();
await Navigation.pop(componentId);
}
} catch (error) {
// RNN returns a promise rejection if there are no screens
// atop the root screen to pop. We'll do nothing in this case.
} }
} }

View File

@@ -36,12 +36,12 @@ export const getIOSAppGroupDetails = (): IOSAppGroupDetails => {
* e.g : * e.g :
* MattermostManaged.deleteDatabaseDirectory(databaseName, shouldRemoveDirectory, (error: any, success: any) => { }); * MattermostManaged.deleteDatabaseDirectory(databaseName, shouldRemoveDirectory, (error: any, success: any) => { });
*/ */
export const deleteIOSDatabase = ({ export const deleteIOSDatabase = async ({
databaseName = undefined, databaseName = undefined,
shouldRemoveDirectory = false, shouldRemoveDirectory = false,
}: IOSDeleteDatabase) => { }: IOSDeleteDatabase) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
MattermostManaged.deleteDatabaseDirectory(databaseName, shouldRemoveDirectory, () => null); return MattermostManaged.deleteDatabaseDirectory(databaseName, shouldRemoveDirectory, () => null);
}; };
/** /**

View File

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Database} from '@nozbe/watermelondb'; import {Database} from '@nozbe/watermelondb';
import Sentry from '@sentry/react-native';
import {Breadcrumb, Event} from '@sentry/types'; import {Breadcrumb, Event} from '@sentry/types';
import {Platform} from 'react-native'; import {Platform} from 'react-native';
import {Navigation} from 'react-native-navigation'; import {Navigation} from 'react-native-navigation';
@@ -19,11 +18,16 @@ import {logError, logWarning} from './log';
export const BREADCRUMB_UNCAUGHT_APP_ERROR = 'uncaught-app-error'; export const BREADCRUMB_UNCAUGHT_APP_ERROR = 'uncaught-app-error';
export const BREADCRUMB_UNCAUGHT_NON_ERROR = 'uncaught-non-error'; export const BREADCRUMB_UNCAUGHT_NON_ERROR = 'uncaught-non-error';
let Sentry: any;
export function initializeSentry() { export function initializeSentry() {
if (!Config.SentryEnabled) { if (!Config.SentryEnabled) {
return; return;
} }
if (!Sentry) {
Sentry = require('@sentry/react-native');
}
const dsn = getDsn(); const dsn = getDsn();
if (!dsn) { if (!dsn) {

View File

@@ -1,12 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import deepEqual from 'deep-equal';
import merge from 'deepmerge'; import merge from 'deepmerge';
import {StatusBar, StyleSheet} from 'react-native'; import {StatusBar, StyleSheet} from 'react-native';
import tinyColor from 'tinycolor2'; import tinyColor from 'tinycolor2';
import {Preferences} from '@constants'; import {Preferences} from '@constants';
import {MODAL_SCREENS_WITHOUT_BACK, SCREENS_WITH_TRANSPARENT_BACKGROUND} from '@constants/screens'; import {MODAL_SCREENS_WITHOUT_BACK, SCREENS_AS_BOTTOM_SHEET, SCREENS_WITH_TRANSPARENT_BACKGROUND} from '@constants/screens';
import EphemeralStore from '@store/ephemeral_store'; import EphemeralStore from '@store/ephemeral_store';
import NavigationStore from '@store/navigation_store'; import NavigationStore from '@store/navigation_store';
import {NamedStyles} from '@typings/global/styles'; import {NamedStyles} from '@typings/global/styles';
@@ -99,13 +100,25 @@ export function setNavigatorStyles(componentId: string, theme: Theme, additional
}, },
}; };
if (!SCREENS_WITH_TRANSPARENT_BACKGROUND.has(componentId)) { if (SCREENS_AS_BOTTOM_SHEET.has(componentId)) {
options.topBar = {
leftButtonColor: changeOpacity(theme.centerChannelColor, 0.56),
background: {
color: theme.centerChannelBg,
},
title: {
color: theme.centerChannelColor,
},
};
}
if (!SCREENS_WITH_TRANSPARENT_BACKGROUND.has(componentId) && !SCREENS_AS_BOTTOM_SHEET.has(componentId)) {
options.layout = { options.layout = {
componentBackgroundColor: theme.centerChannelBg, componentBackgroundColor: theme.centerChannelBg,
}; };
} }
if (!MODAL_SCREENS_WITHOUT_BACK.has(componentId) && options.topBar) { if (!MODAL_SCREENS_WITHOUT_BACK.has(componentId) && !SCREENS_AS_BOTTOM_SHEET.has(componentId) && options.topBar) {
options.topBar.backButton = { options.topBar.backButton = {
color: theme.sidebarHeaderTextColor, color: theme.sidebarHeaderTextColor,
}; };
@@ -263,7 +276,8 @@ export function setThemeDefaults(theme: ExtendedTheme): Theme {
} }
export const updateThemeIfNeeded = (theme: Theme, force = false) => { export const updateThemeIfNeeded = (theme: Theme, force = false) => {
if (theme !== EphemeralStore.theme || force) { const storedTheme = EphemeralStore.theme;
if (!deepEqual(theme, storedTheme) || force) {
EphemeralStore.theme = theme; EphemeralStore.theme = theme;
requestAnimationFrame(() => { requestAnimationFrame(() => {
setNavigationStackStyles(theme); setNavigationStackStyles(theme);

View File

@@ -563,7 +563,6 @@
"mobile.post.delete_title": "Delete Post", "mobile.post.delete_title": "Delete Post",
"mobile.post.failed_delete": "Delete Message", "mobile.post.failed_delete": "Delete Message",
"mobile.post.failed_retry": "Try Again", "mobile.post.failed_retry": "Try Again",
"mobile.posts_view.moreMsg": "More New Messages Above",
"mobile.privacy_link": "Privacy Policy", "mobile.privacy_link": "Privacy Policy",
"mobile.push_notification_reply.button": "Send", "mobile.push_notification_reply.button": "Send",
"mobile.push_notification_reply.placeholder": "Write a reply...", "mobile.push_notification_reply.placeholder": "Write a reply...",

View File

@@ -552,5 +552,7 @@
"channel_info.edit_header": "제목글 편집", "channel_info.edit_header": "제목글 편집",
"channel_info.custom_status": "사용자 정의 상태:", "channel_info.custom_status": "사용자 정의 상태:",
"channel_info.copy_link": "링크 복사", "channel_info.copy_link": "링크 복사",
"general_settings.display": "화면" "general_settings.display": "화면",
"intro.group_message": "여기에서 이 그룹과의 대화가 시작됩니다. 여기에 공유된 메시지와 파일은 다른 사람에게는 보이지 않습니다.",
"intro.direct_message": "이곳에서 {teammate}님과의 대화가 시작됩니다. 여기에 공유된 메시지와 파일은 다른 사람에게는 보이지 않습니다."
} }

View File

@@ -280,7 +280,7 @@
"mobile.reset_status.title_ooo": "\"Ofis dışında\" devre dışı bırakılsın mı?", "mobile.reset_status.title_ooo": "\"Ofis dışında\" devre dışı bırakılsın mı?",
"mobile.routes.code": "{language} kodu", "mobile.routes.code": "{language} kodu",
"mobile.routes.code.noLanguage": "Kod", "mobile.routes.code.noLanguage": "Kod",
"mobile.routes.custom_status": "Bir durum ayarlayın", "mobile.routes.custom_status": "Bir özel durum ayarlayın",
"mobile.routes.table": "Tablo", "mobile.routes.table": "Tablo",
"mobile.routes.user_profile": "Profil", "mobile.routes.user_profile": "Profil",
"mobile.search.jump": "Son iletilere git", "mobile.search.jump": "Son iletilere git",
@@ -467,7 +467,7 @@
"display_settings.clock.military": "24 saat", "display_settings.clock.military": "24 saat",
"custom_status.suggestions.title": "Öneriler", "custom_status.suggestions.title": "Öneriler",
"custom_status.suggestions.recent_title": "Son kullanılanlar", "custom_status.suggestions.recent_title": "Son kullanılanlar",
"custom_status.set_status": "Bir durum ayarlayın", "custom_status.set_status": "Bir özel durum ayarlayın",
"create_post.thread_reply": "Bu konuyu yanıtla...", "create_post.thread_reply": "Bu konuyu yanıtla...",
"create_direct_message.title": "Doğrudan ileti oluştur", "create_direct_message.title": "Doğrudan ileti oluştur",
"channel_modal.purposeEx": "Hataların ve geliştirmelerin bildirileceği bir kanal", "channel_modal.purposeEx": "Hataların ve geliştirmelerin bildirileceği bir kanal",
@@ -644,7 +644,7 @@
"server_upgrade.dismiss": "Yok say", "server_upgrade.dismiss": "Yok say",
"server_upgrade.alert_description": "{serverDisplayName} sunucunuz desteklenmeyen bir sunucu sürümü kullanıyor. Kullanıcılar uyumluluk sorunlarından kaynaklanan çökmeler ya da uygulamanın temel işlevselliğini bozan ciddi sorunlar ile karşılaşacak. Sunucu sürümünün {supportedServerVersion} ya da üzerindeki bir sürüme güncellenmesi gerekli.", "server_upgrade.alert_description": "{serverDisplayName} sunucunuz desteklenmeyen bir sunucu sürümü kullanıyor. Kullanıcılar uyumluluk sorunlarından kaynaklanan çökmeler ya da uygulamanın temel işlevselliğini bozan ciddi sorunlar ile karşılaşacak. Sunucu sürümünün {supportedServerVersion} ya da üzerindeki bir sürüme güncellenmesi gerekli.",
"your.servers": "Sunucularınız", "your.servers": "Sunucularınız",
"video.failed_description": "Görüntü oynatılmaya çalışılırken bir sorun çıktı.\n", "video.failed_description": "Görüntü oynatılmaya çalışılırken bir sorun çıktı.",
"video.download": "Görüntüyü indir", "video.download": "Görüntüyü indir",
"user.tutorial.long_press": "Bir kullanıcının profilini görüntülemek için bir ögeye uzun dokunun", "user.tutorial.long_press": "Bir kullanıcının profilini görüntülemek için bir ögeye uzun dokunun",
"user.settings.notifications.email_threads.description": "İzlediğim konulara yazılan tüm yanıtlar ile ilgili bildirim gönderilsin", "user.settings.notifications.email_threads.description": "İzlediğim konulara yazılan tüm yanıtlar ile ilgili bildirim gönderilsin",
@@ -838,12 +838,44 @@
"connection_banner.connected": "Bağlantı yeniden kuruldu", "connection_banner.connected": "Bağlantı yeniden kuruldu",
"mobile.calls_host": "sunucu", "mobile.calls_host": "sunucu",
"mobile.calls_dismiss": "Yok say", "mobile.calls_dismiss": "Yok say",
"mobile.calls_call_thread": "Kanalı", "mobile.calls_call_thread": "Çağrı konusu",
"mobile.announcement_banner.title": "Duyuru", "mobile.announcement_banner.title": "Duyuru",
"integration_selector.multiselect.submit": "Bitti", "integration_selector.multiselect.submit": "Bitti",
"extension.no_memberships.description": "İçerik paylaşabilmek için bir Mattermost sunucusundaki bir takımın üyesi olmalısınız.", "extension.no_memberships.description": "İçerik paylaşabilmek için bir Mattermost sunucusundaki bir takımın üyesi olmalısınız.",
"general_settings.report_problem": "Sorun bildirin", "general_settings.report_problem": "Sorun bildirin",
"extension.no_servers.title": "Herhangi bir sunucu ile bağlantı kurulmamış", "extension.no_servers.title": "Herhangi bir sunucu ile bağlantı kurulmamış",
"extension.no_servers.description": "İçerik paylaşabilmek için bir Mattermost sunucusundaki oturum açmış olmalısınız.", "extension.no_servers.description": "İçerik paylaşabilmek için bir Mattermost sunucusundaki oturum açmış olmalısınız.",
"extension.no_memberships.title": "Henüz herhangi bir takımın üyesi değil" "extension.no_memberships.title": "Henüz herhangi bir takımın üyesi değil",
"onboarding.integrations": "Sevdiğiniz araçlarla bütünleştirin",
"notification_settings.mentions.keywords_mention": "Anmaları tetikleyecek sözcükler",
"notification_settings.auto_responder.message": "İleti",
"mobile.open_dm.error": "{displayName} ile doğrudan ileti açılamadı. Lütfen bağlantınızı denetleyip yeniden deneyin.",
"mobile.onboarding.sign_in_to_get_started": "Başlamak için oturum açın",
"mobile.onboarding.sign_in": "Oturum aç",
"mobile.onboarding.next": "Sonraki",
"mobile.integration_selector.loading_options": "Ayarlar yükleniyor...",
"mobile.integration_selector.loading_channels": "Kanallar yükleniyor...",
"mobile.custom_list.no_results": "Herhangi bir sonuç bulunamadı",
"mobile.create_direct_message.max_limit_reached": "Grup iletileri {maxCount} üye ile sınırlıdır",
"mobile.camera_type.title": "Kamera ayarları",
"mobile.calls_stop_recording": "Kaydı durdur",
"mobile.calls_request_title": "Çağrılar şu anda etkinleştirilmemiş",
"mobile.calls_request_message": "Çağrılar şu anda deneme kipinde çalışıyor ve bunları yalnızca sistem yöneticileri başlatabilir. Yardım almak için doğrudan sistem yöneticinizle görüşün",
"mobile.calls_record": "Kaydet",
"mobile.calls_rec": "kayıt",
"mobile.calls_react": "Tepki ver",
"mobile.calls_host_rec_title": "Kaydediyorsunuz",
"mobile.calls_participant_rec_title": "Kaydediliyor",
"mobile.calls_participant_rec": "Toplantı sahibi bu toplantıyı kaydetmeye başladı. Toplantıda kalarak kayıt alınmasına onay vermiş olursunuz.",
"mobile.calls_participant_limit_title_GA": "Bu çağrının kapasitesi doldu",
"mobile.calls_open_channel": "Kanalı aç",
"mobile.calls_okay": "Tamam",
"mobile.calls_limit_msg_GA": "{maxParticipants} üzerinde katılımcılı grup aramaları yapabilmek için Cloud Professional ya da Cloud Enterprise tarifesine yükseltin.",
"mobile.calls_host_rec_stopped_title": "Kayıt durduruldu. İşleniyor...",
"mobile.calls_host_rec_stopped": "İşlenmesi tamamlandığında, kaydı bu görüşmenin sohbet konusunda bulabilirsiniz.",
"mobile.calls_host_rec": "Bu toplantıyı kaydediyorsunuz. Bu toplantının kaydedildiğinin herkese bildirilmesi değerlendirin.",
"join_team.error.title": "Bir takıma katılınırken sorun çıktı",
"join_team.error.message": "Takıma katılınırken bir sorun çıktı",
"join_team.error.group_error": "Bu takıma katılmak için ilişkilendirilmiş bir grubun üyesi olmalısınız.",
"connection_banner.connecting": "Bağlantı kuruluyor..."
} }

View File

@@ -1116,7 +1116,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 448; CURRENT_PROJECT_VERSION = 452;
DEVELOPMENT_TEAM = UQ8HT4Q2XM; DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
@@ -1160,7 +1160,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 448; CURRENT_PROJECT_VERSION = 452;
DEVELOPMENT_TEAM = UQ8HT4Q2XM; DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
@@ -1303,7 +1303,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 448; CURRENT_PROJECT_VERSION = 452;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UQ8HT4Q2XM; DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -1354,7 +1354,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 448; CURRENT_PROJECT_VERSION = 452;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UQ8HT4Q2XM; DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;

View File

@@ -37,7 +37,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>448</string> <string>452</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View File

@@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.0.0</string> <string>2.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>448</string> <string>452</string>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>OpenSans-Bold.ttf</string> <string>OpenSans-Bold.ttf</string>

View File

@@ -21,7 +21,7 @@
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.0.0</string> <string>2.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>448</string> <string>452</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>

View File

@@ -157,7 +157,7 @@ class NotificationService: UNNotificationServiceExtension {
os_log( os_log(
OSLogType.default, OSLogType.default,
"Mattermost Notifications: notification receipt failed with status %{public}@. Will call sendMessageIntent", "Mattermost Notifications: notification receipt failed with status %{public}@. Will call sendMessageIntent",
httpResponse.statusCode String(describing: httpResponse.statusCode)
) )
self.sendMessageIntent(notification: self.bestAttemptContent!) self.sendMessageIntent(notification: self.bestAttemptContent!)
return return
@@ -172,7 +172,7 @@ class NotificationService: UNNotificationServiceExtension {
os_log( os_log(
OSLogType.default, OSLogType.default,
"Mattermost Notifications: receipt retrieval failed. Retry %{public}@", "Mattermost Notifications: receipt retrieval failed. Retry %{public}@",
self.retryIndex String(describing: self.retryIndex)
) )
self.fetchReceipt(ackNotification) self.fetchReceipt(ackNotification)
}) })

45
types/database/manager.ts Normal file
View File

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Database} from '@nozbe/watermelondb';
import type {AppDatabase, ServerDatabase, ServerDatabases} from '@typings/database/database';
export type DatabaseManager = {
serverDatabases: ServerDatabases;
updateServerIdentifier: (serverUrl: string, identifier: string, displayName?: string) => Promise<void>;
updateServerDisplayName: (serverUrl: string, displayName: string) => Promise<void>;
isServerPresent: (serverUrl: string) => Promise<boolean>;
getActiveServerUrl: () => Promise<string|undefined>;
getActiveServerDisplayName: () => Promise<string|undefined>;
getServerUrlFromIdentifier: (identifier: string) => Promise<string|undefined>;
getActiveServerDatabase: () => Promise<Database|undefined>;
getAppDatabaseAndOperator: () => AppDatabase|undefined;
getServerDatabaseAndOperator: (serverUrl: string) => ServerDatabase | undefined;
setActiveServerDatabase: (serverUrl: string) => Promise<void>;
deleteServerDatabase: (serverUrl: string) => Promise<void>;
destroyServerDatabase: (serverUrl: string) => Promise<void>;
deleteServerDatabaseFiles: (serverUrl: string) => Promise<void>;
deleteServerDatabaseFilesByName: (databaseName: string) => Promise<void>;
renameDatabase: (databaseName: string, newDBName: string) => Promise<void>;
factoryReset: (shouldRemoveDirectory: boolean) => Promise<boolean>;
getDatabaseFilePath: (dbName: string) => string;
searchUrl: (toFind: string) => string | undefined;
}