Merge branch 'main' of https://github.com/mattermost/mattermost-mobile into MM-42835
This commit is contained in:
commit
5bf0bbbea0
256 changed files with 3885 additions and 2393 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -102,7 +102,7 @@ coverage
|
|||
mattermost-license.txt
|
||||
*.mattermost-license
|
||||
detox/artifacts
|
||||
detox/detox_pixel_4_xl_api_30
|
||||
detox/detox_pixel_*
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
|
|
|||
|
|
@ -54,13 +54,6 @@
|
|||
"error": "visit rvm install https://rvm.io/rvm/install",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "bundler",
|
||||
"semver": "2.1.4",
|
||||
"error": "install watchman `gem install bundler --version 2.1.4`",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "pod",
|
||||
|
|
|
|||
84
NOTICE.txt
84
NOTICE.txt
|
|
@ -200,6 +200,41 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @mattermost/react-native-turbo-mailer
|
||||
|
||||
This product contains '@mattermost/react-native-turbo-mailer' by Avinash Lingaloo.
|
||||
|
||||
An adaptation of react-native-mail that supports Turbo Module
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mattermost/react-native-turbo-mailer#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Mattermost
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @msgpack/msgpack
|
||||
|
|
@ -493,6 +528,21 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @react-navigation/stack
|
||||
|
||||
This product contains '@react-navigation/stack'.
|
||||
|
||||
Stack navigator component for iOS and Android with animated transitions and gestures
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://reactnavigation.org/docs/stack-navigator/
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## @rudderstack/rudder-sdk-react-native
|
||||
|
|
@ -1109,6 +1159,40 @@ Lightweight fuzzy-search
|
|||
limitations under the License.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## html-entities
|
||||
|
||||
This product contains 'html-entities' by Marat Dulin.
|
||||
|
||||
Fastest HTML entities encode/decode library.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mdevils/html-entities#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
Copyright (c) 2021 Dulin Marat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## jail-monkey
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ android {
|
|||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 443
|
||||
versionCode 446
|
||||
versionName "2.0.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
|
|
|||
|
|
@ -74,7 +74,10 @@ public class CustomPushNotificationHelper {
|
|||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, senderId))));
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, null);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
|
@ -270,7 +273,10 @@ public class CustomPushNotificationHelper {
|
|||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, serverUrl, "me"))));
|
||||
Bitmap avatar = userAvatar(context, serverUrl, "me", null);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
|
@ -397,6 +403,7 @@ public class CustomPushNotificationHelper {
|
|||
String channelName = getConversationTitle(bundle);
|
||||
String senderName = bundle.getString("sender_name");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
String urlOverride = bundle.getString("override_icon_url");
|
||||
|
||||
int smallIconResId = getSmallIconResourceId(context, smallIcon);
|
||||
notification.setSmallIcon(smallIconResId);
|
||||
|
|
@ -404,33 +411,46 @@ public class CustomPushNotificationHelper {
|
|||
if (serverUrl != null && channelName.equals(senderName)) {
|
||||
try {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
notification.setLargeIcon(userAvatar(context, serverUrl, senderId));
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
if (avatar != null) {
|
||||
notification.setLargeIcon(avatar);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap userAvatar(Context context, final String serverUrl, final String userId) throws IOException {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
|
||||
private static Bitmap userAvatar(Context context, final String serverUrl, final String userId, final String urlOverride) throws IOException {
|
||||
try {
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
Request request;
|
||||
String url;
|
||||
if (urlOverride != null) {
|
||||
request = new Request.Builder().url(urlOverride).build();
|
||||
Log.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
|
||||
} else {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
|
||||
url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
|
||||
Log.i("ReactNative", String.format("Fetch profile image %s", url));
|
||||
request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
.url(url)
|
||||
.build();
|
||||
}
|
||||
Response response = client.newCall(request).execute();
|
||||
if (response.code() == 200) {
|
||||
assert response.body() != null;
|
||||
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
final String url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
.url(url)
|
||||
.build();
|
||||
Response response = client.newCall(request).execute();
|
||||
if (response.code() == 200) {
|
||||
assert response.body() != null;
|
||||
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
||||
|
||||
Log.i("ReactNative", String.format("Fetch profile %s", url));
|
||||
return getCircleBitmap(bitmap);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public class MainActivity extends NavigationActivity {
|
|||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
super.onCreate(null);
|
||||
setContentView(R.layout.launch_screen);
|
||||
setHWKeyboardConnected();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {addChannelToDefaultCategory, storeCategories} from '@actions/local/categ
|
|||
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
import {Events, General, Preferences, Screens} from '@constants';
|
||||
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {privateChannelJoinPrompt} from '@helpers/api/channel';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
|
|
@ -24,11 +24,11 @@ import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, r
|
|||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from '@utils/channel';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug, logError, logInfo} from '@utils/log';
|
||||
import {showMuteChannelSnackbar} from '@utils/snack_bar';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
import {displayGroupMessageName, displayUsername} from '@utils/user';
|
||||
|
||||
import {fetchGroupsForChannelIfConstrained} from './groups';
|
||||
|
|
@ -360,6 +360,9 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
|
|||
}
|
||||
|
||||
try {
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, true);
|
||||
}
|
||||
const [allChannels, channelMemberships, categoriesWithOrder] = await Promise.all([
|
||||
client.getMyChannels(teamId, includeDeleted, since),
|
||||
client.getMyChannelMembers(teamId),
|
||||
|
|
@ -388,10 +391,14 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
|
|||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
|
||||
return {channels, memberships, categories};
|
||||
} catch (error) {
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
return {error};
|
||||
}
|
||||
|
|
@ -655,7 +662,7 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri
|
|||
let joinedTeam = false;
|
||||
let teamId = '';
|
||||
try {
|
||||
if (teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
if (teamName === DeepLink.Redirect) {
|
||||
teamId = await getCurrentTeamId(database);
|
||||
} else {
|
||||
const team = await getTeamByName(database, teamName);
|
||||
|
|
@ -1257,7 +1264,7 @@ export const unarchiveChannel = async (serverUrl: string, channelId: string) =>
|
|||
try {
|
||||
EphemeralStore.addArchivingChannel(channelId);
|
||||
await client.unarchiveChannel(channelId);
|
||||
await setChannelDeleteAt(serverUrl, channelId, Date.now());
|
||||
await setChannelDeleteAt(serverUrl, channelId, 0);
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
|
|
|
|||
|
|
@ -5,26 +5,18 @@ import {IntlShape} from 'react-intl';
|
|||
import {Alert} from 'react-native';
|
||||
|
||||
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/remote/apps';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import {Client} from '@client/rest';
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
import {AppCallResponseTypes} from '@constants/apps';
|
||||
import DeepLinkType from '@constants/deep_linking';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getConfig, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getTeammateNameDisplay, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {showAppForm, showModal} from '@screens/navigation';
|
||||
import * as DraftUtils from '@utils/draft';
|
||||
import {matchDeepLink, tryOpenURL} from '@utils/url';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import {makeDirectChannel, switchToChannelById, switchToChannelByName} from './channel';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkDM, DeepLinkGM, DeepLinkPlugin} from '@typings/launch';
|
||||
import {showAppForm} from '@screens/navigation';
|
||||
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: string | {message: string}}> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
|
@ -144,60 +136,9 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
|
|||
|
||||
const config = await getConfig(database);
|
||||
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
|
||||
let linkServerUrl: string | undefined;
|
||||
if (match?.data?.serverUrl) {
|
||||
linkServerUrl = DatabaseManager.searchUrl(match.data.serverUrl);
|
||||
}
|
||||
|
||||
if (match && linkServerUrl) {
|
||||
switch (match.type) {
|
||||
case DeepLinkType.Channel: {
|
||||
const data = match.data as DeepLinkChannel;
|
||||
switchToChannelByName(linkServerUrl, data.channelName, data.teamName, DraftUtils.errorBadChannel, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.Permalink: {
|
||||
const data = match.data as DeepLinkPermalink;
|
||||
showPermalink(linkServerUrl, data.teamName, data.postId, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.DirectMessage: {
|
||||
const data = match.data as DeepLinkDM;
|
||||
if (!data.userName) {
|
||||
DraftUtils.errorUnkownUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
if (data.serverUrl !== serverUrl) {
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
}
|
||||
const user = (await queryUsersByUsername(database, [data.userName]).fetch())[0];
|
||||
if (!user) {
|
||||
DraftUtils.errorUnkownUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
makeDirectChannel(linkServerUrl, user.id, displayUsername(user, intl.locale, await getTeammateNameDisplay(database)), true);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.GroupMessage: {
|
||||
const data = match.data as DeepLinkGM;
|
||||
if (!data.channelId) {
|
||||
DraftUtils.errorBadChannel(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
switchToChannelById(linkServerUrl, data.channelId);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.Plugin: {
|
||||
const data = match.data as DeepLinkPlugin;
|
||||
showModal('PluginInternal', data.id, {link: location});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
handleDeepLink(match, intl, location);
|
||||
} else {
|
||||
const {formatMessage} = intl;
|
||||
const onError = () => Alert.alert(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {fetchConfigAndLicense} from '@actions/remote/systems';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
import {logInfo} from '@utils/log';
|
||||
|
||||
|
|
@ -37,8 +38,10 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
|||
const currentChannelId = await getCurrentChannelId(database);
|
||||
const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since;
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +58,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
|
|||
const dt = Date.now();
|
||||
await operator.batchRecords(models);
|
||||
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!;
|
||||
const config = await getConfig(database);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Model} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
|
|
@ -10,11 +9,11 @@ import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
|||
import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest} from '@actions/remote/team';
|
||||
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlAllChannels} from '@client/graphQL/entry';
|
||||
import {Events, General, Preferences, Screens} from '@constants';
|
||||
import {General, Preferences, Screens} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
|
@ -320,9 +319,7 @@ export async function restDeferredAppEntryActions(
|
|||
channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, true);
|
||||
} else {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
|
|
@ -346,7 +343,7 @@ export async function restDeferredAppEntryActions(
|
|||
}
|
||||
}
|
||||
|
||||
await fetchAllTeams(serverUrl);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
await updateAllUsersSince(serverUrl, since);
|
||||
|
||||
// Fetch groups for current user
|
||||
|
|
|
|||
|
|
@ -2,17 +2,16 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {storeConfigAndLicense} from '@actions/local/systems';
|
||||
import {MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {MyTeamsRequest} from '@actions/remote/team';
|
||||
import {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
||||
import {Events, Preferences} from '@constants';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
|
|
@ -49,9 +48,7 @@ export async function deferredAppEntryGraphQLActions(
|
|||
setTimeout(() => {
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, true);
|
||||
} else {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
}
|
||||
}, FETCH_UNREADS_TIMEOUT);
|
||||
|
||||
|
|
@ -98,6 +95,7 @@ export async function deferredAppEntryGraphQLActions(
|
|||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
|
||||
return {error: undefined};
|
||||
|
|
@ -183,9 +181,22 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
|||
user: gqlToClientUser(fetchedData.user!),
|
||||
};
|
||||
|
||||
const allTeams = getMemberTeamsFromGQLQuery(fetchedData);
|
||||
const allTeamMemberships = fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id));
|
||||
|
||||
const [nonArchivedTeams, archivedTeamIds] = allTeams.reduce((acc, t) => {
|
||||
if (t.delete_at) {
|
||||
acc[1].add(t.id);
|
||||
return acc;
|
||||
}
|
||||
return [[...acc[0], t], acc[1]];
|
||||
}, [[], new Set<string>()]);
|
||||
|
||||
const nonArchivedTeamMemberships = allTeamMemberships.filter((m) => !archivedTeamIds.has(m.team_id));
|
||||
|
||||
const teamData = {
|
||||
teams: getMemberTeamsFromGQLQuery(fetchedData),
|
||||
memberships: fetchedData.teamMembers.map((m) => gqlToClientTeamMembership(m, meData.user.id)),
|
||||
teams: nonArchivedTeams,
|
||||
memberships: nonArchivedTeamMemberships,
|
||||
};
|
||||
|
||||
const prefData = {
|
||||
|
|
@ -205,7 +216,7 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
|
|||
let initialTeamId = currentTeamId;
|
||||
if (!teamData.teams.length) {
|
||||
initialTeamId = '';
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId)) {
|
||||
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId && t.delete_at === 0)) {
|
||||
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
|
||||
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {fetchConfigAndLicense} from '@actions/remote/systems';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
|
|
@ -47,9 +48,11 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
|
|||
return {error: clData.error};
|
||||
}
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, '', '');
|
||||
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +69,7 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
|
|||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const config = clData.config || {} as ClientConfig;
|
||||
const license = clData.license || {} as ClientLicense;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {getIsCRTEnabled} from '@queries/servers/thread';
|
|||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
|
||||
|
|
@ -81,8 +82,10 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
|||
switchedToScreen = true;
|
||||
}
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, teamId, channelId);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
}
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
|
@ -134,6 +137,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
|||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
|
||||
const config = await getConfig(database);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {getMyChannel, getChannelById} from '@queries/servers/channel';
|
|||
import {getCurrentTeamId, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
|
||||
const fetchNotificationData = async (serverUrl: string, notification: NotificationWithData, skipEvents = false) => {
|
||||
|
|
@ -122,6 +123,7 @@ export const openNotification = async (serverUrl: string, notification: Notifica
|
|||
}
|
||||
|
||||
try {
|
||||
EphemeralStore.setNotificationTapped(true);
|
||||
const {database} = operator;
|
||||
const channelId = notification.payload!.channel_id!;
|
||||
const rootId = notification.payload!.root_id!;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeepLink} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCurrentTeam} from '@queries/servers/team';
|
||||
import {displayPermalink} from '@utils/permalink';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export const showPermalink = async (serverUrl: string, teamName: string, postId: string, intl: IntlShape, openAsPermalink = true) => {
|
||||
export const showPermalink = async (serverUrl: string, teamName: string, postId: string, openAsPermalink = true) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
|
|
@ -18,7 +17,7 @@ export const showPermalink = async (serverUrl: string, teamName: string, postId:
|
|||
try {
|
||||
let name = teamName;
|
||||
let team: TeamModel | undefined;
|
||||
if (!name || name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
if (!name || name === DeepLink.Redirect) {
|
||||
team = await getCurrentTeam(database);
|
||||
if (team) {
|
||||
name = team.name;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import {getPostById, getRecentPostsInChannel} from '@queries/servers/post';
|
|||
import {getCurrentUserId, getCurrentChannelId} from '@queries/servers/system';
|
||||
import {getIsCRTEnabled, prepareThreadsFromReceivedPosts} from '@queries/servers/thread';
|
||||
import {queryAllUsers} from '@queries/servers/user';
|
||||
import {setFetchingThreadState} from '@store/fetching_thread_store';
|
||||
import {getValidEmojis, matchEmoticons} from '@utils/emoji/helpers';
|
||||
import {isServerError} from '@utils/errors';
|
||||
import {logError} from '@utils/log';
|
||||
import {processPostsFetched} from '@utils/post';
|
||||
import {getPostIdsForCombinedUserActivityPost} from '@utils/post_list';
|
||||
|
|
@ -133,7 +135,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
|||
let created;
|
||||
try {
|
||||
created = await client.createPost(newPost);
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
const errorPost = {
|
||||
...newPost,
|
||||
id: pendingPostId,
|
||||
|
|
@ -146,10 +148,11 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
|||
|
||||
// If the failure was because: the root post was deleted or
|
||||
// TownSquareIsReadOnly=true then remove the post
|
||||
if (error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
if (isServerError(error) && (
|
||||
error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
error.server_error_id === ServerErrors.TOWN_SQUARE_READ_ONLY_ERROR ||
|
||||
error.server_error_id === ServerErrors.PLUGIN_DISMISSED_POST_ERROR
|
||||
) {
|
||||
)) {
|
||||
await removePost(serverUrl, databasePost);
|
||||
} else {
|
||||
const models = await operator.handlePosts({
|
||||
|
|
@ -251,11 +254,12 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
|
|||
}
|
||||
}
|
||||
await operator.batchRecords(models);
|
||||
} catch (error: any) {
|
||||
if (error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
} catch (error) {
|
||||
if (isServerError(error) && (
|
||||
error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
|
||||
error.server_error_id === ServerErrors.TOWN_SQUARE_READ_ONLY_ERROR ||
|
||||
error.server_error_id === ServerErrors.PLUGIN_DISMISSED_POST_ERROR
|
||||
) {
|
||||
)) {
|
||||
await removePost(serverUrl, post);
|
||||
} else {
|
||||
post.prepareUpdate((p) => {
|
||||
|
|
@ -325,12 +329,9 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
|
|||
}
|
||||
}
|
||||
|
||||
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string, emitEvent = false) => {
|
||||
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string) => {
|
||||
try {
|
||||
const promises = [];
|
||||
if (emitEvent) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, true);
|
||||
}
|
||||
for (const member of memberships) {
|
||||
const channel = channels.find((c) => c.id === member.channel_id);
|
||||
if (channel && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
|
||||
|
|
@ -338,13 +339,7 @@ export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: C
|
|||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
if (emitEvent) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
}
|
||||
} catch (error) {
|
||||
if (emitEvent) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
}
|
||||
return {error};
|
||||
}
|
||||
|
||||
|
|
@ -583,6 +578,8 @@ export async function fetchPostThread(serverUrl: string, postId: string, options
|
|||
return {error};
|
||||
}
|
||||
|
||||
setFetchingThreadState(postId, true);
|
||||
|
||||
try {
|
||||
const isCRTEnabled = await getIsCRTEnabled(operator.database);
|
||||
|
||||
|
|
@ -620,9 +617,11 @@ export async function fetchPostThread(serverUrl: string, postId: string, options
|
|||
}
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
setFetchingThreadState(postId, false);
|
||||
return {posts: extractRecordsForTable<PostModel>(posts, MM_TABLES.SERVER.POST)};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
|
||||
setFetchingThreadState(postId, false);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import {Model} from '@nozbe/watermelondb';
|
|||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
|
||||
import {Client} from '@client/rest';
|
||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||
import {Events} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
|
@ -12,9 +14,10 @@ import {getActiveServerUrl} from '@queries/app/servers';
|
|||
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam, getDefaultChannelForTeam} from '@queries/servers/channel';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getCurrentUserId} from '@queries/servers/system';
|
||||
import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, getNthLastChannelFromTeam, queryTeamsById, syncTeamTable, getLastTeam, getTeamById, removeTeamFromTeamHistory} from '@queries/servers/team';
|
||||
import {dismissAllModals, popToRoot, resetToTeams} from '@screens/navigation';
|
||||
import {addTeamToTeamHistory, prepareDeleteTeam, prepareMyTeams, getNthLastChannelFromTeam, queryTeamsById, getLastTeam, getTeamById, removeTeamFromTeamHistory, queryMyTeams} from '@queries/servers/team';
|
||||
import {dismissAllModals, popToRoot} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
|
|
@ -56,12 +59,16 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
|||
return {error};
|
||||
}
|
||||
|
||||
let loadEventSent = false;
|
||||
try {
|
||||
EphemeralStore.startAddingToTeam(teamId);
|
||||
const team = await client.getTeam(teamId);
|
||||
const member = await client.addToTeam(teamId, userId);
|
||||
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, true);
|
||||
loadEventSent = true;
|
||||
|
||||
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
|
||||
const {channels, memberships: channelMembers, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
|
@ -80,6 +87,8 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
|||
])).flat();
|
||||
|
||||
await operator.batchRecords(models);
|
||||
setTeamLoading(serverUrl, false);
|
||||
loadEventSent = false;
|
||||
|
||||
if (await isTablet()) {
|
||||
const channel = await getDefaultChannelForTeam(operator.database, teamId);
|
||||
|
|
@ -87,11 +96,18 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
|||
fetchPostsForChannel(serverUrl, channel.id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setTeamLoading(serverUrl, false);
|
||||
loadEventSent = false;
|
||||
}
|
||||
}
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
return {member};
|
||||
} catch (error) {
|
||||
if (loadEventSent) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
|
|
@ -222,23 +238,10 @@ export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly =
|
|||
}
|
||||
}
|
||||
|
||||
export const fetchAllTeams = async (serverUrl: string, fetchOnly = false): Promise<MyTeamsRequest> => {
|
||||
let client;
|
||||
export const fetchAllTeams = async (serverUrl: string, page = 0, perPage = PER_PAGE_DEFAULT): Promise<{teams?: Team[]; error?: any}> => {
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const teams = await client.getTeams();
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
syncTeamTable(operator, teams);
|
||||
}
|
||||
}
|
||||
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const teams = await client.getTeams(page, perPage);
|
||||
return {teams};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
|
|
@ -246,6 +249,76 @@ export const fetchAllTeams = async (serverUrl: string, fetchOnly = false): Promi
|
|||
}
|
||||
};
|
||||
|
||||
const recCanJoinTeams = async (client: Client, myTeamsIds: Set<string>, page: number): Promise<boolean> => {
|
||||
const fetchedTeams = await client.getTeams(page, PER_PAGE_DEFAULT);
|
||||
if (fetchedTeams.find((t) => !myTeamsIds.has(t.id) && t.delete_at === 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fetchedTeams.length === PER_PAGE_DEFAULT) {
|
||||
return recCanJoinTeams(client, myTeamsIds, page + 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const LOAD_MORE_THRESHOLD = 10;
|
||||
export async function fetchTeamsForComponent(
|
||||
serverUrl: string,
|
||||
page: number,
|
||||
joinedIds?: Set<string>,
|
||||
alreadyLoaded: Team[] = [],
|
||||
): Promise<{teams: Team[]; hasMore: boolean; page: number}> {
|
||||
let hasMore = true;
|
||||
const {teams, error} = await fetchAllTeams(serverUrl, page, PER_PAGE_DEFAULT);
|
||||
if (error || !teams || teams.length < PER_PAGE_DEFAULT) {
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {teams: alreadyLoaded, hasMore, page};
|
||||
}
|
||||
|
||||
if (teams?.length) {
|
||||
const notJoinedTeams = joinedIds ? teams.filter((t) => !joinedIds.has(t.id)) : teams;
|
||||
alreadyLoaded.push(...notJoinedTeams);
|
||||
|
||||
if (teams.length < PER_PAGE_DEFAULT) {
|
||||
hasMore = false;
|
||||
}
|
||||
|
||||
if (
|
||||
hasMore &&
|
||||
(alreadyLoaded.length > LOAD_MORE_THRESHOLD)
|
||||
) {
|
||||
return fetchTeamsForComponent(serverUrl, page + 1, joinedIds, alreadyLoaded);
|
||||
}
|
||||
|
||||
return {teams: alreadyLoaded, hasMore, page: page + 1};
|
||||
}
|
||||
|
||||
return {teams: alreadyLoaded, hasMore: false, page};
|
||||
}
|
||||
|
||||
export const updateCanJoinTeams = async (serverUrl: string) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
const myTeamsIds = new Set(myTeams.map((m) => m.id));
|
||||
|
||||
const canJoin = await recCanJoinTeams(client, myTeamsIds, 0);
|
||||
|
||||
EphemeralStore.setCanJoinOtherTeams(serverUrl, canJoin);
|
||||
return {};
|
||||
} catch (error) {
|
||||
EphemeralStore.setCanJoinOtherTeams(serverUrl, false);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, since: number, teams: Team[], memberships: TeamMembership[], excludeTeamId?: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
|
|
@ -315,7 +388,7 @@ export const removeUserFromTeam = async (serverUrl: string, teamId: string, user
|
|||
|
||||
if (!fetchOnly) {
|
||||
localRemoveUserFromTeam(serverUrl, teamId);
|
||||
fetchAllTeams(serverUrl);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
|
|
@ -388,9 +461,9 @@ export async function handleKickFromTeam(serverUrl: string, teamId: string) {
|
|||
const teamToJumpTo = await getLastTeam(database, teamId);
|
||||
if (teamToJumpTo) {
|
||||
await handleTeamChange(serverUrl, teamToJumpTo);
|
||||
} else if (currentServer === serverUrl) {
|
||||
await resetToTeams();
|
||||
}
|
||||
|
||||
// Resetting to team select handled by the home screen
|
||||
} catch (error) {
|
||||
logDebug('Failed to kick user from team', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
|
|
@ -25,15 +27,24 @@ export async function fetchTermsOfService(serverUrl: string): Promise<{terms?: T
|
|||
}
|
||||
|
||||
export async function updateTermsOfServiceStatus(serverUrl: string, id: string, status: boolean): Promise<{resp?: {status: string}; error?: any}> {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const resp = await client.updateMyTermsOfServiceStatus(id, status);
|
||||
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
if (currentUser) {
|
||||
currentUser.prepareUpdate((u) => {
|
||||
if (status) {
|
||||
u.termsOfServiceCreateAt = Date.now();
|
||||
u.termsOfServiceId = id;
|
||||
} else {
|
||||
u.termsOfServiceCreateAt = 0;
|
||||
u.termsOfServiceId = '';
|
||||
}
|
||||
});
|
||||
operator.batchRecords([currentUser]);
|
||||
}
|
||||
return {resp};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {markChannelAsViewed} from '@actions/local/channel';
|
||||
import {markChannelAsRead} from '@actions/remote/channel';
|
||||
import {handleEntryAfterLoadNavigation} from '@actions/remote/entry/common';
|
||||
|
|
@ -30,11 +28,10 @@ import {
|
|||
handleCallUserVoiceOn,
|
||||
} from '@calls/connection/websocket_event_handlers';
|
||||
import {isSupportedServerCalls} from '@calls/utils';
|
||||
import {Events, Screens, WebsocketEvents} from '@constants';
|
||||
import {Screens, WebsocketEvents} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import {getActiveServerUrl} from '@queries/app/servers';
|
||||
import {getCurrentChannel} from '@queries/servers/channel';
|
||||
import {getLastPostInThread} from '@queries/servers/post';
|
||||
import {
|
||||
|
|
@ -50,6 +47,7 @@ import {getIsCRTEnabled} from '@queries/servers/thread';
|
|||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug, logInfo} from '@utils/log';
|
||||
|
||||
|
|
@ -70,7 +68,7 @@ import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePrefe
|
|||
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
|
||||
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
|
||||
import {handleLicenseChangedEvent, handleConfigChangedEvent} from './system';
|
||||
import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent} from './teams';
|
||||
import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent, handleTeamArchived, handleTeamRestored} from './teams';
|
||||
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
|
||||
import {handleUserUpdatedEvent, handleUserTypingEvent} from './users';
|
||||
|
||||
|
|
@ -88,7 +86,7 @@ export async function handleFirstConnect(serverUrl: string) {
|
|||
|
||||
// ESR: 5.37
|
||||
if (lastDisconnect && config?.EnableReliableWebSockets !== 'true' && alreadyConnected.has(serverUrl)) {
|
||||
handleReconnect(serverUrl);
|
||||
await handleReconnect(serverUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -102,8 +100,8 @@ export async function handleFirstConnect(serverUrl: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export function handleReconnect(serverUrl: string) {
|
||||
doReconnect(serverUrl);
|
||||
export async function handleReconnect(serverUrl: string) {
|
||||
await doReconnect(serverUrl);
|
||||
}
|
||||
|
||||
export async function handleClose(serverUrl: string, lastDisconnect: number) {
|
||||
|
|
@ -141,15 +139,10 @@ async function doReconnect(serverUrl: string) {
|
|||
const currentTeam = await getCurrentTeam(database);
|
||||
const currentChannel = await getCurrentChannel(database);
|
||||
|
||||
const currentActiveServerUrl = await getActiveServerUrl();
|
||||
if (serverUrl === currentActiveServerUrl) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, true);
|
||||
}
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt);
|
||||
if ('error' in entryData) {
|
||||
if (serverUrl === currentActiveServerUrl) {
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
}
|
||||
setTeamLoading(serverUrl, false);
|
||||
return;
|
||||
}
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
|
@ -159,6 +152,7 @@ async function doReconnect(serverUrl: string) {
|
|||
const dt = Date.now();
|
||||
await operator.batchRecords(models);
|
||||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
await fetchPostDataIfNeeded(serverUrl);
|
||||
|
||||
|
|
@ -318,6 +312,14 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
|||
handleOpenDialogEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.DELETE_TEAM:
|
||||
handleTeamArchived(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.RESTORE_TEAM:
|
||||
handleTeamRestored(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.THREAD_UPDATED:
|
||||
handleThreadUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
|
@ -441,7 +443,10 @@ async function fetchPostDataIfNeeded(serverUrl: string) {
|
|||
if (currentChannelId && (isChannelScreenMounted || tabletDevice)) {
|
||||
await fetchPostsForChannel(serverUrl, currentChannelId);
|
||||
markChannelAsRead(serverUrl, currentChannelId);
|
||||
markChannelAsViewed(serverUrl, currentChannelId);
|
||||
if (!EphemeralStore.wasNotificationTapped()) {
|
||||
markChannelAsViewed(serverUrl, currentChannelId);
|
||||
}
|
||||
EphemeralStore.setNotificationTapped(false);
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('could not fetch needed post after WS reconnect', error);
|
||||
|
|
|
|||
|
|
@ -6,16 +6,77 @@ import {Model} from '@nozbe/watermelondb';
|
|||
import {removeUserFromTeam} from '@actions/local/team';
|
||||
import {fetchMyChannelsForTeam} from '@actions/remote/channel';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchAllTeams, fetchMyTeam, handleKickFromTeam} from '@actions/remote/team';
|
||||
import {fetchMyTeam, handleKickFromTeam, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {getCurrentTeam, prepareMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentTeam, prepareMyTeams, queryMyTeamsByIds} from '@queries/servers/team';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
export async function handleTeamArchived(serverUrl: string, msg: WebSocketMessage) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const team: Team = JSON.parse(msg.data.team);
|
||||
|
||||
const membership = (await queryMyTeamsByIds(database, [team.id]).fetch())[0];
|
||||
if (membership) {
|
||||
const currentTeam = await getCurrentTeam(database);
|
||||
if (currentTeam?.id === team.id) {
|
||||
await handleKickFromTeam(serverUrl, team.id);
|
||||
}
|
||||
|
||||
await removeUserFromTeam(serverUrl, team.id);
|
||||
|
||||
const user = await getCurrentUser(database);
|
||||
if (user?.isGuest) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
}
|
||||
}
|
||||
updateCanJoinTeams(serverUrl);
|
||||
} catch (error) {
|
||||
logDebug('cannot handle archive team websocket event', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleTeamRestored(serverUrl: string, msg: WebSocketMessage) {
|
||||
let markedAsLoading = false;
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const team: Team = JSON.parse(msg.data.team);
|
||||
|
||||
const teamMembership = await client.getTeamMember(team.id, 'me');
|
||||
if (teamMembership && teamMembership.delete_at === 0) {
|
||||
// Ignore duplicated team join events sent by the server
|
||||
if (EphemeralStore.isAddingToTeam(team.id)) {
|
||||
return;
|
||||
}
|
||||
EphemeralStore.startAddingToTeam(team.id);
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
markedAsLoading = true;
|
||||
await fetchAndStoreJoinedTeamInfo(serverUrl, operator, team.id, [team], [teamMembership]);
|
||||
setTeamLoading(serverUrl, false);
|
||||
markedAsLoading = false;
|
||||
|
||||
EphemeralStore.finishAddingToTeam(team.id);
|
||||
}
|
||||
|
||||
updateCanJoinTeams(serverUrl);
|
||||
} catch (error) {
|
||||
if (markedAsLoading) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
logDebug('cannot handle restore team websocket event', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -33,7 +94,7 @@ export async function handleLeaveTeamEvent(serverUrl: string, msg: WebSocketMess
|
|||
}
|
||||
|
||||
await removeUserFromTeam(serverUrl, teamId);
|
||||
fetchAllTeams(serverUrl);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
|
||||
if (user.isGuest) {
|
||||
updateUsersNoLongerVisible(serverUrl);
|
||||
|
|
@ -62,10 +123,6 @@ export async function handleUpdateTeamEvent(serverUrl: string, msg: WebSocketMes
|
|||
}
|
||||
|
||||
export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
const {team_id: teamId} = msg.data;
|
||||
|
||||
// Ignore duplicated team join events sent by the server
|
||||
|
|
@ -74,8 +131,20 @@ export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSock
|
|||
}
|
||||
EphemeralStore.startAddingToTeam(teamId);
|
||||
|
||||
const {teams, memberships: teamMemberships} = await fetchMyTeam(serverUrl, teamId, true);
|
||||
try {
|
||||
setTeamLoading(serverUrl, true);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {teams, memberships: teamMemberships} = await fetchMyTeam(serverUrl, teamId, true);
|
||||
|
||||
await fetchAndStoreJoinedTeamInfo(serverUrl, operator, teamId, teams, teamMemberships);
|
||||
} catch (error) {
|
||||
logDebug('could not handle user added to team websocket event');
|
||||
}
|
||||
setTeamLoading(serverUrl, false);
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
}
|
||||
|
||||
const fetchAndStoreJoinedTeamInfo = async (serverUrl: string, operator: ServerDataOperator, teamId: string, teams?: Team[], teamMemberships?: TeamMembership[]) => {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (teams?.length && teamMemberships?.length) {
|
||||
const {channels, memberships, categories} = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
|
|
@ -83,7 +152,9 @@ export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSock
|
|||
modelPromises.push(...await prepareMyChannelsForTeam(operator, teamId, channels || [], memberships || []));
|
||||
|
||||
const {roles} = await fetchRoles(serverUrl, teamMemberships, memberships, undefined, true);
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
if (roles?.length) {
|
||||
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
}
|
||||
|
||||
if (teams && teamMemberships) {
|
||||
|
|
@ -92,6 +163,4 @@ export async function handleUserAddedToTeamEvent(serverUrl: string, msg: WebSock
|
|||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
|
||||
EphemeralStore.finishAddingToTeam(teamId);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import React, {useCallback} from 'react';
|
|||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import ChannelInfoStartButton from '@calls/components/channel_info_start';
|
||||
import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
import CopyChannelLinkBox from '@components/channel_actions/copy_channel_link_box';
|
||||
import FavoriteBox from '@components/channel_actions/favorite_box';
|
||||
import MutedBox from '@components/channel_actions/mute_box';
|
||||
|
|
@ -14,6 +13,8 @@ import {useServerUrl} from '@context/server';
|
|||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {isTypeDMorGM} from '@utils/channel';
|
||||
|
||||
// import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
channelType?: ChannelType;
|
||||
|
|
@ -69,6 +70,7 @@ const ChannelActions = ({channelId, channelType, inModal = false, dismissChannel
|
|||
testID={`${testID}.set_header.action`}
|
||||
/>
|
||||
}
|
||||
{/* Add back in after MM-47655 is resolved. https://mattermost.atlassian.net/browse/MM-47655
|
||||
{!isDM &&
|
||||
<AddPeopleBox
|
||||
channelId={channelId}
|
||||
|
|
@ -76,6 +78,7 @@ const ChannelActions = ({channelId, channelType, inModal = false, dismissChannel
|
|||
testID={`${testID}.add_people.action`}
|
||||
/>
|
||||
}
|
||||
*/}
|
||||
{!isDM && !callsEnabled &&
|
||||
<>
|
||||
<View style={styles.separator}/>
|
||||
|
|
|
|||
|
|
@ -15,19 +15,20 @@ import {showSnackBar} from '@utils/snack_bar';
|
|||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
bottomSheetId: typeof Screens[keyof typeof Screens];
|
||||
sourceScreen: typeof Screens[keyof typeof Screens];
|
||||
post: PostModel;
|
||||
teamName: string;
|
||||
}
|
||||
const CopyPermalinkOption = ({teamName, post, sourceScreen}: Props) => {
|
||||
const CopyPermalinkOption = ({bottomSheetId, teamName, post, sourceScreen}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const handleCopyLink = useCallback(async () => {
|
||||
const permalink = `${serverUrl}/${teamName}/pl/${post.id}`;
|
||||
Clipboard.setString(permalink);
|
||||
await dismissBottomSheet(Screens.POST_OPTIONS);
|
||||
await dismissBottomSheet(bottomSheetId);
|
||||
showSnackBar({barType: SNACK_BAR_TYPE.LINK_COPIED, sourceScreen});
|
||||
}, [teamName, post.id]);
|
||||
}, [teamName, post.id, bottomSheetId]);
|
||||
|
||||
return (
|
||||
<BaseOption
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
|
||||
import {updateThreadFollowing} from '@actions/remote/thread';
|
||||
import {BaseOption} from '@components/common_post_options';
|
||||
|
|
@ -13,11 +13,12 @@ import {dismissBottomSheet} from '@screens/navigation';
|
|||
import type ThreadModel from '@typings/database/models/servers/thread';
|
||||
|
||||
type FollowThreadOptionProps = {
|
||||
bottomSheetId: typeof Screens[keyof typeof Screens];
|
||||
thread: ThreadModel;
|
||||
teamId?: string;
|
||||
};
|
||||
|
||||
const FollowThreadOption = ({thread, teamId}: FollowThreadOptionProps) => {
|
||||
const FollowThreadOption = ({bottomSheetId, thread, teamId}: FollowThreadOptionProps) => {
|
||||
let id: string;
|
||||
let defaultMessage: string;
|
||||
let icon: string;
|
||||
|
|
@ -44,13 +45,13 @@ const FollowThreadOption = ({thread, teamId}: FollowThreadOptionProps) => {
|
|||
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const handleToggleFollow = async () => {
|
||||
const handleToggleFollow = useCallback(async () => {
|
||||
if (teamId == null) {
|
||||
return;
|
||||
}
|
||||
await dismissBottomSheet(Screens.POST_OPTIONS);
|
||||
await dismissBottomSheet(bottomSheetId);
|
||||
updateThreadFollowing(serverUrl, teamId, thread.id, !thread.isFollowing);
|
||||
};
|
||||
}, [bottomSheetId, teamId, thread]);
|
||||
|
||||
const followThreadOptionTestId = thread.isFollowing ? 'post_options.following_thread.option' : 'post_options.follow_thread.option';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@ import type PostModel from '@typings/database/models/servers/post';
|
|||
|
||||
type Props = {
|
||||
post: PostModel;
|
||||
location?: typeof Screens[keyof typeof Screens];
|
||||
bottomSheetId: typeof Screens[keyof typeof Screens];
|
||||
}
|
||||
const ReplyOption = ({post, location}: Props) => {
|
||||
const ReplyOption = ({post, bottomSheetId}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const handleReply = useCallback(async () => {
|
||||
const rootId = post.rootId || post.id;
|
||||
await dismissBottomSheet(location || Screens.POST_OPTIONS);
|
||||
await dismissBottomSheet(bottomSheetId);
|
||||
fetchAndSwitchToThread(serverUrl, rootId);
|
||||
}, [post, serverUrl]);
|
||||
}, [bottomSheetId, post, serverUrl]);
|
||||
|
||||
return (
|
||||
<BaseOption
|
||||
|
|
|
|||
|
|
@ -11,18 +11,19 @@ import {t} from '@i18n';
|
|||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
|
||||
type CopyTextProps = {
|
||||
bottomSheetId: typeof Screens[keyof typeof Screens];
|
||||
isSaved: boolean;
|
||||
postId: string;
|
||||
}
|
||||
|
||||
const SaveOption = ({isSaved, postId}: CopyTextProps) => {
|
||||
const SaveOption = ({bottomSheetId, isSaved, postId}: CopyTextProps) => {
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const onHandlePress = useCallback(async () => {
|
||||
const remoteAction = isSaved ? deleteSavedPost : savePostPreference;
|
||||
await dismissBottomSheet(Screens.POST_OPTIONS);
|
||||
await dismissBottomSheet(bottomSheetId);
|
||||
remoteAction(serverUrl, postId);
|
||||
}, [postId, serverUrl]);
|
||||
}, [bottomSheetId, postId, serverUrl]);
|
||||
|
||||
const id = isSaved ? t('mobile.post_info.unsave') : t('mobile.post_info.save');
|
||||
const defaultMessage = isSaved ? 'Unsave' : 'Save';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
|
|||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
isConnected: boolean;
|
||||
websocketState: WebsocketConnectedState;
|
||||
}
|
||||
|
||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
|
@ -74,7 +74,7 @@ const TIME_TO_OPEN = toMilliseconds({seconds: 3});
|
|||
const TIME_TO_CLOSE = toMilliseconds({seconds: 1});
|
||||
|
||||
const ConnectionBanner = ({
|
||||
isConnected,
|
||||
websocketState,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const closeTimeout = useRef<NodeJS.Timeout | null>();
|
||||
|
|
@ -86,6 +86,8 @@ const ConnectionBanner = ({
|
|||
const appState = useAppState();
|
||||
const netInfo = useNetInfo();
|
||||
|
||||
const isConnected = websocketState === 'connected';
|
||||
|
||||
const openCallback = useCallback(() => {
|
||||
setVisible(true);
|
||||
clearTimeoutRef(openTimeout);
|
||||
|
|
@ -97,7 +99,9 @@ const ConnectionBanner = ({
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) {
|
||||
if (websocketState === 'connecting') {
|
||||
openCallback();
|
||||
} else if (!isConnected) {
|
||||
openTimeout.current = setTimeout(openCallback, TIME_TO_OPEN);
|
||||
}
|
||||
return () => {
|
||||
|
|
@ -158,6 +162,8 @@ const ConnectionBanner = ({
|
|||
let text;
|
||||
if (isConnected) {
|
||||
text = intl.formatMessage({id: 'connection_banner.connected', defaultMessage: 'Connection restored'});
|
||||
} else if (websocketState === 'connecting') {
|
||||
text = intl.formatMessage({id: 'connection_banner.connecting', defaultMessage: 'Connecting...'});
|
||||
} else if (netInfo.isInternetReachable) {
|
||||
text = intl.formatMessage({id: 'connection_banner.not_reachable', defaultMessage: 'The server is not reachable'});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import websocket_manager from '@managers/websocket_manager';
|
|||
import ConnectionBanner from './connection_banner';
|
||||
|
||||
const enhanced = withObservables(['serverUrl'], ({serverUrl}: {serverUrl: string}) => ({
|
||||
isConnected: websocket_manager.observeConnected(serverUrl),
|
||||
websocketState: websocket_manager.observeWebsocketState(serverUrl),
|
||||
}));
|
||||
|
||||
export default withServerUrl(enhanced(ConnectionBanner));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import {DeviceEventEmitter, StyleProp, StyleSheet, View, ViewStyle} from 'react-native';
|
||||
import Animated, {useDerivedValue} from 'react-native-reanimated';
|
||||
|
||||
|
|
@ -69,10 +69,9 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
filesForGallery.value[idx] = file;
|
||||
};
|
||||
|
||||
const isSingleImage = () => (filesInfo.length === 1 && (isImage(filesInfo[0]) || isVideo(filesInfo[0])));
|
||||
const isSingleImage = useMemo(() => filesInfo.filter((f) => isImage(f) || isVideo(f)).length === 1, [filesInfo]);
|
||||
|
||||
const renderItems = (items: FileInfo[], moreImagesCount = 0, includeGutter = false) => {
|
||||
const singleImage = isSingleImage();
|
||||
let nonVisibleImagesCount: number;
|
||||
let container: StyleProp<ViewStyle> = items.length > 1 ? styles.container : undefined;
|
||||
const containerWithGutter = [container, styles.gutter];
|
||||
|
|
@ -97,7 +96,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l
|
|||
file={file}
|
||||
index={attachmentIndex(file.id!)}
|
||||
onPress={handlePreviewPress}
|
||||
isSingleImage={singleImage}
|
||||
isSingleImage={isSingleImage}
|
||||
nonVisibleImagesCount={nonVisibleImagesCount}
|
||||
publicLinkEnabled={publicLinkEnabled}
|
||||
updateFileForGallery={updateFileForGallery}
|
||||
|
|
|
|||
|
|
@ -76,9 +76,8 @@ const VideoFile = ({
|
|||
const viewPortHeight = Math.max(dimensions.height, dimensions.width) * 0.45;
|
||||
return calculateDimensions(video.height || wrapperWidth, video.width || wrapperWidth, wrapperWidth, viewPortHeight);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [dimensions.height, dimensions.width, video.height, video.width, wrapperWidth]);
|
||||
}, [dimensions.height, dimensions.width, video.height, video.width, wrapperWidth, isSingleImage]);
|
||||
|
||||
const getThumbnail = async () => {
|
||||
const data = {...file};
|
||||
|
|
@ -161,7 +160,12 @@ const VideoFile = ({
|
|||
|
||||
if (failed) {
|
||||
thumbnail = (
|
||||
<View style={[isSingleImage ? null : style.imagePreview, style.failed, imageDimensions]}>
|
||||
<View
|
||||
|
||||
// @ts-expect-error ref of type unknown
|
||||
ref={forwardRef}
|
||||
style={[isSingleImage ? null : {height: '100%'}, style.failed, imageDimensions]}
|
||||
>
|
||||
<FileIcon
|
||||
failed={failed}
|
||||
file={file}
|
||||
|
|
@ -174,7 +178,7 @@ const VideoFile = ({
|
|||
<View
|
||||
style={style.fileImageWrapper}
|
||||
>
|
||||
{!isSingleImage && <View style={style.boxPlaceholder}/>}
|
||||
{!isSingleImage && !failed && <View style={style.boxPlaceholder}/>}
|
||||
{thumbnail}
|
||||
<View style={style.playContainer}>
|
||||
<View style={style.play}>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@
|
|||
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useState, useEffect, useRef, useImperativeHandle, forwardRef, useMemo, useCallback} from 'react';
|
||||
import {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, Platform, StyleProp, TargetedEvent, Text, TextInput, TextInputFocusEventData, TextInputProps, TextStyle, TouchableWithoutFeedback, View, ViewStyle} from 'react-native';
|
||||
import {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TargetedEvent, Text, TextInput, TextInputFocusEventData, TextInputProps, TextStyle, TouchableWithoutFeedback, View, ViewStyle} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming, Easing} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import {getLabelPositions, onExecution} from './utils';
|
||||
|
||||
const DEFAULT_INPUT_HEIGHT = 48;
|
||||
const BORDER_DEFAULT_WIDTH = 1;
|
||||
const BORDER_FOCUSED_WIDTH = 2;
|
||||
|
|
@ -36,6 +38,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
lineHeight: 16,
|
||||
paddingVertical: 5,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
flex: 1,
|
||||
paddingHorizontal: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
label: {
|
||||
position: 'absolute',
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
|
|
@ -52,6 +62,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
fontSize: 10,
|
||||
},
|
||||
textInput: {
|
||||
flexDirection: 'row',
|
||||
fontFamily: 'OpenSans',
|
||||
fontSize: 16,
|
||||
paddingTop: 12,
|
||||
|
|
@ -65,29 +76,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const onExecution = (
|
||||
e: NativeSyntheticEvent<TextInputFocusEventData>,
|
||||
innerFunc?: () => void,
|
||||
outerFunc?: ((event: NativeSyntheticEvent<TargetedEvent>) => void),
|
||||
) => {
|
||||
innerFunc?.();
|
||||
outerFunc?.(e);
|
||||
};
|
||||
|
||||
const getLabelPositions = (style: TextStyle, labelStyle: TextStyle, smallLabelStyle: TextStyle) => {
|
||||
const top: number = style.paddingTop as number || 0;
|
||||
const bottom: number = style.paddingBottom as number || 0;
|
||||
|
||||
const height: number = (style.height as number || (top + bottom) || style.padding as number) || 0;
|
||||
const textInputFontSize = style.fontSize || 13;
|
||||
const labelFontSize = labelStyle.fontSize || 16;
|
||||
const smallLabelFontSize = smallLabelStyle.fontSize || 10;
|
||||
const fontSizeDiff = textInputFontSize - labelFontSize;
|
||||
const unfocused = (height * 0.5) + (fontSizeDiff * (Platform.OS === 'android' ? 0.5 : 0.6));
|
||||
const focused = -(labelFontSize + smallLabelFontSize) * 0.25;
|
||||
return [unfocused, focused];
|
||||
};
|
||||
|
||||
export type FloatingTextInputRef = {
|
||||
blur: () => void;
|
||||
focus: () => void;
|
||||
|
|
@ -97,10 +85,11 @@ export type FloatingTextInputRef = {
|
|||
type FloatingTextInputProps = TextInputProps & {
|
||||
containerStyle?: ViewStyle;
|
||||
editable?: boolean;
|
||||
endAdornment?: React.ReactNode;
|
||||
error?: string;
|
||||
errorIcon?: string;
|
||||
isKeyboardInput?: boolean;
|
||||
label?: string;
|
||||
label: string;
|
||||
labelTextStyle?: TextStyle;
|
||||
multiline?: boolean;
|
||||
onBlur?: (event: NativeSyntheticEvent<TargetedEvent>) => void;
|
||||
|
|
@ -120,6 +109,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||
editable = true,
|
||||
error,
|
||||
errorIcon = 'alert-outline',
|
||||
endAdornment,
|
||||
isKeyboardInput = true,
|
||||
label = '',
|
||||
labelTextStyle,
|
||||
|
|
@ -192,7 +182,7 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||
return res;
|
||||
}, [styles, containerStyle, multiline]);
|
||||
|
||||
const combinedTextInputStyle = useMemo(() => {
|
||||
const combinedTextInputContainerStyle = useMemo(() => {
|
||||
const res: StyleProp<TextStyle> = [styles.textInput];
|
||||
if (!editable) {
|
||||
res.push(styles.readOnly);
|
||||
|
|
@ -217,6 +207,16 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||
return res;
|
||||
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline, editable]);
|
||||
|
||||
const combinedTextInputStyle = useMemo(() => {
|
||||
const res: StyleProp<TextStyle> = [styles.textInput, styles.input, textInputStyle];
|
||||
|
||||
if (multiline) {
|
||||
res.push({height: 80, textAlignVertical: 'top'});
|
||||
}
|
||||
|
||||
return res;
|
||||
}, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline, editable]);
|
||||
|
||||
const textAnimatedTextStyle = useAnimatedStyle(() => {
|
||||
const inputText = placeholder || value || props.defaultValue;
|
||||
const index = inputText || focusedLabel ? 1 : 0;
|
||||
|
|
@ -245,31 +245,32 @@ const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProp
|
|||
onLayout={onLayout}
|
||||
>
|
||||
<View style={combinedContainerStyle}>
|
||||
{label && (
|
||||
<Animated.Text
|
||||
onPress={onAnimatedTextPress}
|
||||
style={[styles.label, labelTextStyle, textAnimatedTextStyle]}
|
||||
suppressHighlighting={true}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{label}
|
||||
</Animated.Text>
|
||||
)}
|
||||
<TextInput
|
||||
{...props}
|
||||
editable={isKeyboardInput && editable}
|
||||
style={combinedTextInputStyle}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={styles.label.color}
|
||||
multiline={multiline}
|
||||
value={value}
|
||||
pointerEvents={isKeyboardInput ? 'auto' : 'none'}
|
||||
onFocus={onTextInputFocus}
|
||||
onBlur={onTextInputBlur}
|
||||
ref={inputRef}
|
||||
underlineColorAndroid='transparent'
|
||||
testID={testID}
|
||||
/>
|
||||
<Animated.Text
|
||||
onPress={onAnimatedTextPress}
|
||||
style={[styles.label, labelTextStyle, textAnimatedTextStyle]}
|
||||
suppressHighlighting={true}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{label}
|
||||
</Animated.Text>
|
||||
<View style={combinedTextInputContainerStyle}>
|
||||
<TextInput
|
||||
{...props}
|
||||
editable={isKeyboardInput && editable}
|
||||
style={combinedTextInputStyle}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={styles.label.color}
|
||||
multiline={multiline}
|
||||
value={value}
|
||||
pointerEvents={isKeyboardInput ? 'auto' : 'none'}
|
||||
onFocus={onTextInputFocus}
|
||||
onBlur={onTextInputBlur}
|
||||
ref={inputRef}
|
||||
underlineColorAndroid='transparent'
|
||||
testID={testID}
|
||||
/>
|
||||
{endAdornment}
|
||||
</View>
|
||||
{Boolean(error) && (
|
||||
<View style={styles.errorContainer}>
|
||||
{showErrorIcon && errorIcon &&
|
||||
|
|
|
|||
28
app/components/floating_text_input_label/utils.ts
Normal file
28
app/components/floating_text_input_label/utils.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {NativeSyntheticEvent, Platform, TargetedEvent, TextInputFocusEventData, TextStyle} from 'react-native';
|
||||
|
||||
export const onExecution = (
|
||||
e: NativeSyntheticEvent<TextInputFocusEventData>,
|
||||
innerFunc?: () => void,
|
||||
outerFunc?: ((event: NativeSyntheticEvent<TargetedEvent>) => void),
|
||||
) => {
|
||||
innerFunc?.();
|
||||
outerFunc?.(e);
|
||||
};
|
||||
|
||||
export const getLabelPositions = (style: TextStyle, labelStyle: TextStyle, smallLabelStyle: TextStyle) => {
|
||||
const top: number = style.paddingTop as number || 0;
|
||||
const bottom: number = style.paddingBottom as number || 0;
|
||||
|
||||
const height: number = (style.height as number || (top + bottom) || style.padding as number) || 0;
|
||||
const textInputFontSize = style.fontSize || 13;
|
||||
const labelFontSize = labelStyle.fontSize || 16;
|
||||
const smallLabelFontSize = smallLabelStyle.fontSize || 10;
|
||||
const fontSizeDiff = textInputFontSize - labelFontSize;
|
||||
const unfocused = (height * 0.5) + (fontSizeDiff * (Platform.OS === 'android' ? 0.5 : 0.6));
|
||||
const focused = -(labelFontSize + smallLabelFontSize) * 0.25;
|
||||
return [unfocused, focused];
|
||||
};
|
||||
|
||||
|
|
@ -2,8 +2,9 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, Text, TextStyle} from 'react-native';
|
||||
import {DeviceEventEmitter, StyleProp, Text, TextStyle} from 'react-native';
|
||||
|
||||
import {Navigation, Screens} from '@constants';
|
||||
import {popToRoot, dismissAllModals} from '@screens/navigation';
|
||||
|
||||
type HashtagProps = {
|
||||
|
|
@ -17,7 +18,12 @@ const Hashtag = ({hashtag, linkStyle}: HashtagProps) => {
|
|||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
|
||||
// showSearchModal('#' + hashtag);
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATE_TO_TAB, {
|
||||
screen: Screens.SEARCH,
|
||||
params: {
|
||||
searchTerm: hashtag,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import {Parser, Node} from 'commonmark';
|
||||
import Renderer from 'commonmark-react-renderer';
|
||||
import React, {ReactElement, useMemo, useRef} from 'react';
|
||||
|
|
@ -131,6 +132,7 @@ const Markdown = ({
|
|||
textStyles = {}, theme, value = '', baseParagraphStyle,
|
||||
}: MarkdownProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
|
||||
const urlFilter = (url: string) => {
|
||||
const scheme = getScheme(url);
|
||||
|
|
@ -471,10 +473,14 @@ const Markdown = ({
|
|||
};
|
||||
|
||||
const renderText = ({context, literal}: MarkdownBaseRenderer) => {
|
||||
const selectable = (managedConfig.copyAndPasteProtection !== 'true') && context.includes('table_cell');
|
||||
if (context.indexOf('image') !== -1) {
|
||||
// If this text is displayed, it will be styled by the image component
|
||||
return (
|
||||
<Text testID='markdown_text'>
|
||||
<Text
|
||||
testID='markdown_text'
|
||||
selectable={selectable}
|
||||
>
|
||||
{literal}
|
||||
</Text>
|
||||
);
|
||||
|
|
@ -496,6 +502,7 @@ const Markdown = ({
|
|||
<Text
|
||||
testID='markdown_text'
|
||||
style={styles}
|
||||
selectable={selectable}
|
||||
>
|
||||
{literal}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
height: 24,
|
||||
},
|
||||
container: {
|
||||
marginBottom: 5,
|
||||
marginVertical: 5,
|
||||
top: 5,
|
||||
},
|
||||
svg: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
|
||||
|
|
|
|||
|
|
@ -9,19 +9,14 @@ import {Alert, StyleSheet, Text, View} from 'react-native';
|
|||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {switchToChannelByName} from '@actions/remote/channel';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import DeepLinkType from '@constants/deep_linking';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
|
||||
import {errorBadChannel} from '@utils/draft';
|
||||
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {matchDeepLink, normalizeProtocol, tryOpenURL} from '@utils/url';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkWithData} from '@typings/launch';
|
||||
import {normalizeProtocol, tryOpenURL} from '@utils/url';
|
||||
|
||||
type MarkdownLinkProps = {
|
||||
children: ReactElement;
|
||||
|
|
@ -65,28 +60,27 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
|
|||
return;
|
||||
}
|
||||
|
||||
const match: DeepLinkWithData | null = matchDeepLink(url, serverUrl, siteURL);
|
||||
const onError = () => {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.link.error.title',
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.link.error.text',
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (match && match.data?.teamName) {
|
||||
if (match.type === DeepLinkType.Channel) {
|
||||
await switchToChannelByName(serverUrl, (match?.data as DeepLinkChannel).channelName, match.data?.teamName, errorBadChannel, intl);
|
||||
} else if (match.type === DeepLinkType.Permalink) {
|
||||
showPermalink(serverUrl, match.data.teamName, (match.data as DeepLinkPermalink).postId, intl);
|
||||
const match = matchDeepLink(url, serverUrl, siteURL);
|
||||
|
||||
if (match) {
|
||||
const {error} = await handleDeepLink(match, intl);
|
||||
if (error) {
|
||||
tryOpenURL(match, onError);
|
||||
}
|
||||
} else {
|
||||
const onError = () => {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.link.error.title',
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.link.error.text',
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
tryOpenURL(url, onError);
|
||||
}
|
||||
}), [href, intl.locale, serverUrl, siteURL]);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
|||
justifyContent: 'flex-start',
|
||||
padding: 8,
|
||||
},
|
||||
textContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
cellRightBorder: {
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
|
|
@ -57,7 +62,9 @@ const MarkdownTableCell = ({isLastCell, align, children}: MarkdownTableCellProps
|
|||
style={[cellStyle, textStyle]}
|
||||
testID='markdown_table_cell'
|
||||
>
|
||||
{children}
|
||||
<View style={style.textContainer}>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -283,7 +283,6 @@ const OptionItem = ({
|
|||
}
|
||||
</View>
|
||||
);
|
||||
|
||||
if (Object.values(TouchableOptionTypes).includes(type)) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ type Props = {
|
|||
canShowPostPriority?: boolean;
|
||||
|
||||
// Post Props
|
||||
postProps: Post['props'];
|
||||
updatePostProps: (postProps: Post['props']) => void;
|
||||
postPriority: PostPriorityData;
|
||||
updatePostPriority: (postPriority: PostPriorityData) => void;
|
||||
|
||||
// Cursor Position Handler
|
||||
updateCursorPosition: React.Dispatch<React.SetStateAction<number>>;
|
||||
|
|
@ -110,8 +110,8 @@ export default function DraftInput({
|
|||
updateCursorPosition,
|
||||
cursorPosition,
|
||||
updatePostInputTop,
|
||||
postProps,
|
||||
updatePostProps,
|
||||
postPriority,
|
||||
updatePostPriority,
|
||||
setIsFocused,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
|
|
@ -155,9 +155,9 @@ export default function DraftInput({
|
|||
overScrollMode={'never'}
|
||||
disableScrollViewPanResponder={true}
|
||||
>
|
||||
{Boolean(postProps.priority) && (
|
||||
{Boolean(postPriority?.priority) && (
|
||||
<View style={style.postPriorityLabel}>
|
||||
<PostPriorityLabel label={postProps.priority}/>
|
||||
<PostPriorityLabel label={postPriority!.priority}/>
|
||||
</View>
|
||||
)}
|
||||
<PostInput
|
||||
|
|
@ -188,8 +188,8 @@ export default function DraftInput({
|
|||
addFiles={addFiles}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
postProps={postProps}
|
||||
updatePostProps={updatePostProps}
|
||||
postPriority={postPriority}
|
||||
updatePostPriority={updatePostPriority}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
focus={focus}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,60 +2,36 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
import {CameraOptions} from 'react-native-image-picker';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
onPress: (options: CameraOptions) => void;
|
||||
}
|
||||
|
||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
center: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 200,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
options: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-evenly',
|
||||
width: '100%',
|
||||
marginBottom: 50,
|
||||
},
|
||||
optionContainer: {
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
const CameraType = ({onPress}: Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const style = getStyle(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
const onPhoto = async () => {
|
||||
const options: CameraOptions = {
|
||||
|
|
@ -80,54 +56,26 @@ const CameraType = ({onPress}: Props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View>
|
||||
{!isTablet &&
|
||||
<FormattedText
|
||||
id='camera_type.title'
|
||||
defaultMessage='Choose an action'
|
||||
id='mobile.camera_type.title'
|
||||
defaultMessage='Camera options'
|
||||
style={style.title}
|
||||
/>
|
||||
}
|
||||
<View style={style.options}>
|
||||
<View style={style.flex}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onPhoto}
|
||||
testID='camera_type.photo'
|
||||
>
|
||||
<View style={style.center}>
|
||||
<CompassIcon
|
||||
color={theme.centerChannelColor}
|
||||
name='camera-outline'
|
||||
size={38}
|
||||
/>
|
||||
<FormattedText
|
||||
id='camera_type.photo.option'
|
||||
defaultMessage='Capture Photo'
|
||||
style={style.text}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
<View style={style.flex}>
|
||||
<TouchableWithFeedback
|
||||
onPress={onVideo}
|
||||
testID='camera_type.video'
|
||||
>
|
||||
<View style={style.center}>
|
||||
<CompassIcon
|
||||
color={theme.centerChannelColor}
|
||||
name='video-outline'
|
||||
size={38}
|
||||
/>
|
||||
<FormattedText
|
||||
id='camera_type.video.option'
|
||||
defaultMessage='Record Video'
|
||||
style={style.text}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
</View>
|
||||
<SlideUpPanelItem
|
||||
icon='camera-outline'
|
||||
onPress={onPhoto}
|
||||
testID='camera_type.photo'
|
||||
text={intl.formatMessage({id: 'camera_type.photo.option', defaultMessage: 'Capture Photo'})}
|
||||
/>
|
||||
<SlideUpPanelItem
|
||||
icon='video-outline'
|
||||
onPress={onVideo}
|
||||
testID='camera_type.video'
|
||||
text={intl.formatMessage({id: 'camera_type.video.option', defaultMessage: 'Record Video'})}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,14 +5,18 @@ import React, {useCallback} from 'react';
|
|||
import {useIntl} from 'react-intl';
|
||||
import {Alert, StyleSheet} from 'react-native';
|
||||
import {CameraOptions} from 'react-native-image-picker';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {ICON_SIZE} from '@constants/post_draft';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {TITLE_HEIGHT} from '@screens/bottom_sheet/content';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {fileMaxWarning} from '@utils/file';
|
||||
import PickerUtil from '@utils/file/file_picker';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import CameraType from './camera_type';
|
||||
|
|
@ -36,6 +40,7 @@ export default function CameraQuickAction({
|
|||
}: QuickActionAttachmentProps) {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
|
||||
const handleButtonPress = useCallback((options: CameraOptions) => {
|
||||
const picker = new PickerUtil(intl,
|
||||
|
|
@ -64,14 +69,15 @@ export default function CameraQuickAction({
|
|||
return;
|
||||
}
|
||||
|
||||
const snap = bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom);
|
||||
bottomSheet({
|
||||
title: intl.formatMessage({id: 'camera_type.title', defaultMessage: 'Choose an action'}),
|
||||
title: intl.formatMessage({id: 'mobile.camera_type.title', defaultMessage: 'Camera options'}),
|
||||
renderContent,
|
||||
snapPoints: [200, 10],
|
||||
snapPoints: [TITLE_HEIGHT + snap, 10],
|
||||
theme,
|
||||
closeButtonId: 'camera-close-id',
|
||||
});
|
||||
}, [intl, theme, renderContent, maxFilesReached, maxFileCount]);
|
||||
}, [intl, theme, renderContent, maxFilesReached, maxFileCount, bottom]);
|
||||
|
||||
const actionTestID = disabled ? `${testID}.disabled` : testID;
|
||||
const color = disabled ? changeOpacity(theme.centerChannelColor, 0.16) : changeOpacity(theme.centerChannelColor, 0.64);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {useIntl} from 'react-intl';
|
|||
import {StyleSheet} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import PostPriorityPicker, {PostPriorityData} from '@components/post_priority/post_priority_picker';
|
||||
import PostPriorityPicker from '@components/post_priority/post_priority_picker';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {ICON_SIZE} from '@constants/post_draft';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
|
@ -15,8 +15,8 @@ import {changeOpacity} from '@utils/theme';
|
|||
|
||||
type Props = {
|
||||
testID?: string;
|
||||
postProps: Post['props'];
|
||||
updatePostProps: (postProps: Post['props']) => void;
|
||||
postPriority: PostPriorityData;
|
||||
updatePostPriority: (postPriority: PostPriorityData) => void;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
|
|
@ -29,30 +29,27 @@ const style = StyleSheet.create({
|
|||
|
||||
export default function PostPriorityAction({
|
||||
testID,
|
||||
postProps,
|
||||
updatePostProps,
|
||||
postPriority,
|
||||
updatePostPriority,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
const handlePostPriorityPicker = useCallback((postPriorityData: PostPriorityData) => {
|
||||
updatePostProps((oldPostProps: Post['props']) => ({
|
||||
...oldPostProps,
|
||||
...postPriorityData,
|
||||
}));
|
||||
updatePostPriority(postPriorityData);
|
||||
dismissBottomSheet();
|
||||
}, [updatePostProps]);
|
||||
}, [updatePostPriority]);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
return (
|
||||
<PostPriorityPicker
|
||||
data={{
|
||||
priority: postProps?.priority || '',
|
||||
priority: postPriority?.priority || '',
|
||||
}}
|
||||
onSubmit={handlePostPriorityPicker}
|
||||
/>
|
||||
);
|
||||
}, [handlePostPriorityPicker, postProps]);
|
||||
}, [handlePostPriorityPicker, postPriority]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
bottomSheet({
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ type Props = {
|
|||
value: string;
|
||||
updateValue: (value: string) => void;
|
||||
addFiles: (file: FileInfo[]) => void;
|
||||
postProps: Post['props'];
|
||||
updatePostProps: (postProps: Post['props']) => void;
|
||||
postPriority: PostPriorityData;
|
||||
updatePostPriority: (postPriority: PostPriorityData) => void;
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -45,8 +45,8 @@ export default function QuickActions({
|
|||
maxFileCount,
|
||||
updateValue,
|
||||
addFiles,
|
||||
postProps,
|
||||
updatePostProps,
|
||||
postPriority,
|
||||
updatePostPriority,
|
||||
focus,
|
||||
}: Props) {
|
||||
const atDisabled = value[value.length - 1] === '@';
|
||||
|
|
@ -101,8 +101,8 @@ export default function QuickActions({
|
|||
{isPostPriorityEnabled && canShowPostPriority && (
|
||||
<PostPriorityAction
|
||||
testID={postPriorityActionTestID}
|
||||
postProps={postProps}
|
||||
updatePostProps={updatePostProps}
|
||||
postPriority={postPriority}
|
||||
updatePostPriority={updatePostPriority}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {setStatus} from '@actions/remote/user';
|
|||
import {canEndCall, endCall, getEndCallMessage} from '@calls/actions/calls';
|
||||
import ClientError from '@client/rest/error';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {PostPriorityType} from '@constants/post';
|
||||
import {NOTIFY_ALL_MEMBERS} from '@constants/post_draft';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import DraftUploadManager from '@managers/draft_upload_manager';
|
||||
|
|
@ -54,6 +55,10 @@ type Props = {
|
|||
uploadFileError: React.ReactNode;
|
||||
}
|
||||
|
||||
const INITIAL_PRIORITY = {
|
||||
priority: PostPriorityType.STANDARD,
|
||||
};
|
||||
|
||||
export default function SendHandler({
|
||||
testID,
|
||||
channelId,
|
||||
|
|
@ -83,8 +88,7 @@ export default function SendHandler({
|
|||
|
||||
const [channelTimezoneCount, setChannelTimezoneCount] = useState(0);
|
||||
const [sendingMessage, setSendingMessage] = useState(false);
|
||||
|
||||
const [postProps, setPostProps] = useState<Post['props']>({});
|
||||
const [postPriority, setPostPriority] = useState<PostPriorityData>(INITIAL_PRIORITY);
|
||||
|
||||
const canSend = useCallback(() => {
|
||||
if (sendingMessage) {
|
||||
|
|
@ -120,17 +124,19 @@ export default function SendHandler({
|
|||
message: value,
|
||||
} as Post;
|
||||
|
||||
if (Object.keys(postProps).length) {
|
||||
post.props = postProps;
|
||||
if (Object.keys(postPriority).length) {
|
||||
post.metadata = {
|
||||
priority: postPriority,
|
||||
};
|
||||
}
|
||||
|
||||
createPost(serverUrl, post, postFiles);
|
||||
|
||||
clearDraft();
|
||||
setSendingMessage(false);
|
||||
setPostProps({});
|
||||
setPostPriority(INITIAL_PRIORITY);
|
||||
DeviceEventEmitter.emit(Events.POST_LIST_SCROLL_TO_BOTTOM, rootId ? Screens.THREAD : Screens.CHANNEL);
|
||||
}, [files, currentUserId, channelId, rootId, value, clearDraft, postProps]);
|
||||
}, [files, currentUserId, channelId, rootId, value, clearDraft, postPriority]);
|
||||
|
||||
const showSendToAllOrChannelOrHereAlert = useCallback((calculatedMembersCount: number, atHere: boolean) => {
|
||||
const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(intl, calculatedMembersCount, Boolean(isTimezoneEnabled), channelTimezoneCount, atHere);
|
||||
|
|
@ -297,8 +303,8 @@ export default function SendHandler({
|
|||
canSend={canSend()}
|
||||
maxMessageLength={maxMessageLength}
|
||||
updatePostInputTop={updatePostInputTop}
|
||||
postProps={postProps}
|
||||
updatePostProps={setPostProps}
|
||||
postPriority={postPriority}
|
||||
updatePostPriority={setPostPriority}
|
||||
setIsFocused={setIsFocused}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
message: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200),
|
||||
lineHeight: undefined, // remove line height, not needed and causes problems with md images
|
||||
},
|
||||
pendingPost: {
|
||||
opacity: 0.5,
|
||||
|
|
|
|||
|
|
@ -132,10 +132,10 @@ const Header = (props: HeaderProps) => {
|
|||
style={style.time}
|
||||
testID='post_header.date_time'
|
||||
/>
|
||||
{showPostPriority && (
|
||||
{showPostPriority && post.metadata?.priority?.priority && (
|
||||
<View style={style.postPriority}>
|
||||
<PostPriorityLabel
|
||||
label={post.props?.priority}
|
||||
label={post.metadata.priority.priority}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ const Post = ({
|
|||
|
||||
const handlePostPress = () => {
|
||||
if ([Screens.SAVED_MESSAGES, Screens.MENTIONS, Screens.SEARCH, Screens.PINNED_MESSAGES].includes(location)) {
|
||||
showPermalink(serverUrl, '', post.id, intl);
|
||||
showPermalink(serverUrl, '', post.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +226,7 @@ const Post = ({
|
|||
// If the post is a priority post:
|
||||
// 1. Show the priority label in channel screen
|
||||
// 2. Show the priority label in thread screen for the root post
|
||||
const showPostPriority = Boolean(isPostPriorityEnabled && post.props?.priority) && (location !== Screens.THREAD || !post.rootId);
|
||||
const showPostPriority = Boolean(isPostPriorityEnabled && post.metadata?.priority?.priority) && (location !== Screens.THREAD || !post.rootId);
|
||||
|
||||
const sameSequence = hasReplies ? (hasReplies && post.rootId) : !post.rootId;
|
||||
if (!showPostPriority && hasSameRoot && isConsecutivePost && sameSequence) {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] =
|
|||
</Text>
|
||||
</Text>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
|
|
@ -106,6 +107,7 @@ exports[`renderSystemMessage uses renderer for Channel Header update 1`] = `
|
|||
</Text>
|
||||
</Text>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
|
|
@ -184,6 +186,7 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 1
|
|||
</Text>
|
||||
</Text>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
|
|
@ -246,6 +249,7 @@ exports[`renderSystemMessage uses renderer for Guest added and join to channel 2
|
|||
</Text>
|
||||
</Text>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
|
|
@ -308,6 +312,7 @@ exports[`renderSystemMessage uses renderer for OLD archived channel without a us
|
|||
>
|
||||
<Text>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
|
|
@ -370,6 +375,7 @@ exports[`renderSystemMessage uses renderer for archived channel 1`] = `
|
|||
</Text>
|
||||
</Text>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
|
|
@ -432,6 +438,7 @@ exports[`renderSystemMessage uses renderer for unarchived channel 1`] = `
|
|||
</Text>
|
||||
</Text>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.6)",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type Props = {
|
|||
currentTimezone: string | null;
|
||||
currentUserId: string;
|
||||
currentUsername: string;
|
||||
disablePullToRefresh?: boolean;
|
||||
highlightedId?: PostModel['id'];
|
||||
highlightPinnedOrSaved?: boolean;
|
||||
isCRTEnabled?: boolean;
|
||||
|
|
@ -45,6 +46,7 @@ type Props = {
|
|||
showMoreMessages?: boolean;
|
||||
showNewMessageLine?: boolean;
|
||||
footer?: ReactElement;
|
||||
header?: ReactElement;
|
||||
testID: string;
|
||||
currentCallBarVisible?: boolean;
|
||||
joinCallBannerVisible?: boolean;
|
||||
|
|
@ -84,7 +86,9 @@ const PostList = ({
|
|||
currentTimezone,
|
||||
currentUserId,
|
||||
currentUsername,
|
||||
disablePullToRefresh,
|
||||
footer,
|
||||
header,
|
||||
highlightedId,
|
||||
highlightPinnedOrSaved = true,
|
||||
isCRTEnabled,
|
||||
|
|
@ -365,7 +369,7 @@ const PostList = ({
|
|||
return (
|
||||
<>
|
||||
<PostListRefreshControl
|
||||
enabled={enableRefreshControl}
|
||||
enabled={!disablePullToRefresh && enableRefreshControl}
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
style={styles.container}
|
||||
|
|
@ -377,6 +381,7 @@ const PostList = ({
|
|||
keyboardShouldPersistTaps='handled'
|
||||
keyExtractor={keyExtractor}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER + 5}
|
||||
ListHeaderComponent={header}
|
||||
ListFooterComponent={footer}
|
||||
maintainVisibleContentPosition={SCROLL_POSITION_CONFIG}
|
||||
maxToRenderPerBatch={10}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ describe('ThreadOverview', () => {
|
|||
const props = {
|
||||
isSaved: true,
|
||||
repliesCount: 0,
|
||||
rootId: '',
|
||||
rootPost: {} as PostModel,
|
||||
testID: 'thread-overview',
|
||||
};
|
||||
|
|
@ -26,6 +27,7 @@ describe('ThreadOverview', () => {
|
|||
const props = {
|
||||
isSaved: false,
|
||||
repliesCount: 2,
|
||||
rootId: '',
|
||||
rootPost: {} as PostModel,
|
||||
testID: 'thread-overview',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {Screens} from '@constants';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {useFetchingThreadState} from '@hooks/fetching_thread';
|
||||
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -23,6 +24,7 @@ import type PostModel from '@typings/database/models/servers/post';
|
|||
type Props = {
|
||||
isSaved: boolean;
|
||||
repliesCount: number;
|
||||
rootId: string;
|
||||
rootPost?: PostModel;
|
||||
testID: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
|
|
@ -56,13 +58,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const ThreadOverview = ({isSaved, repliesCount, rootPost, style, testID}: Props) => {
|
||||
const ThreadOverview = ({isSaved, repliesCount, rootId, rootPost, style, testID}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const serverUrl = useServerUrl();
|
||||
const isFetchingThread = useFetchingThreadState(rootId);
|
||||
|
||||
const onHandleSavePress = useCallback(preventDoubleTap(() => {
|
||||
if (rootPost?.id) {
|
||||
|
|
@ -98,30 +101,44 @@ const ThreadOverview = ({isSaved, repliesCount, rootPost, style, testID}: Props)
|
|||
|
||||
const saveButtonTestId = isSaved ? `${testID}.unsave.button` : `${testID}.save.button`;
|
||||
|
||||
let repliesCountElement;
|
||||
if (repliesCount > 0) {
|
||||
repliesCountElement = (
|
||||
<FormattedText
|
||||
style={styles.repliesCount}
|
||||
id='thread.repliesCount'
|
||||
defaultMessage='{repliesCount, number} {repliesCount, plural, one {reply} other {replies}}'
|
||||
testID={`${testID}.replies_count`}
|
||||
values={{repliesCount}}
|
||||
/>
|
||||
);
|
||||
} else if (isFetchingThread) {
|
||||
repliesCountElement = (
|
||||
<FormattedText
|
||||
style={styles.repliesCount}
|
||||
id='thread.loadingReplies'
|
||||
defaultMessage='Loading replies...'
|
||||
testID={`${testID}.loading_replies`}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
repliesCountElement = (
|
||||
<FormattedText
|
||||
style={styles.repliesCount}
|
||||
id='thread.noReplies'
|
||||
defaultMessage='No replies yet'
|
||||
testID={`${testID}.no_replies`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={containerStyle}
|
||||
testID={testID}
|
||||
>
|
||||
<View style={styles.repliesCountContainer}>
|
||||
{
|
||||
repliesCount > 0 ? (
|
||||
<FormattedText
|
||||
style={styles.repliesCount}
|
||||
id='thread.repliesCount'
|
||||
defaultMessage='{repliesCount, number} {repliesCount, plural, one {reply} other {replies}}'
|
||||
testID={`${testID}.replies_count`}
|
||||
values={{repliesCount}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedText
|
||||
style={styles.repliesCount}
|
||||
id='thread.noReplies'
|
||||
defaultMessage='No replies yet'
|
||||
testID={`${testID}.no_replies`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{repliesCountElement}
|
||||
</View>
|
||||
<View style={styles.optionsContainer}>
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const style = StyleSheet.create({
|
|||
});
|
||||
|
||||
type Props = {
|
||||
label: PostPriorityType;
|
||||
label: PostPriorityData['priority'];
|
||||
};
|
||||
|
||||
const PostPriorityLabel = ({label}: Props) => {
|
||||
|
|
@ -48,7 +48,7 @@ const PostPriorityLabel = ({label}: Props) => {
|
|||
containerStyle.push(style.urgent);
|
||||
iconName = 'alert-outline';
|
||||
labelText = intl.formatMessage({id: 'post_priority.label.urgent', defaultMessage: 'URGENT'});
|
||||
} else {
|
||||
} else if (label === PostPriorityType.IMPORTANT) {
|
||||
containerStyle.push(style.important);
|
||||
iconName = 'alert-circle-outline';
|
||||
labelText = intl.formatMessage({id: 'post_priority.label.important', defaultMessage: 'IMPORTANT'});
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ import {typography} from '@utils/typography';
|
|||
|
||||
import PostPriorityPickerItem from './post_priority_picker_item';
|
||||
|
||||
export type PostPriorityData = {
|
||||
priority: PostPriorityType;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
data: PostPriorityData;
|
||||
onSubmit: (data: PostPriorityData) => void;
|
||||
|
|
@ -34,7 +30,7 @@ const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 600, 'SemiBold'),
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
betaContainer: {
|
||||
backgroundColor: PostPriorityColors.IMPORTANT,
|
||||
|
|
@ -61,8 +57,8 @@ const PostPriorityPicker = ({data, onSubmit}: Props) => {
|
|||
// For now, we just have one option but the spec suggest we have more in the next phase
|
||||
// const [data, setData] = React.useState<PostPriorityData>(defaultData);
|
||||
|
||||
const handleUpdatePriority = React.useCallback((priority: PostPriorityType) => {
|
||||
onSubmit({priority});
|
||||
const handleUpdatePriority = React.useCallback((priority: PostPriorityData['priority']) => {
|
||||
onSubmit({priority: priority || ''});
|
||||
}, [onSubmit]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -67,6 +67,11 @@ type Props = {
|
|||
*/
|
||||
teammateNameDisplay: string;
|
||||
|
||||
/**
|
||||
* test ID
|
||||
*/
|
||||
testID?: string;
|
||||
|
||||
/**
|
||||
* toast Icon
|
||||
*/
|
||||
|
|
@ -130,7 +135,7 @@ export default function SelectedUsers({
|
|||
buttonIcon, buttonText, containerHeight = 0,
|
||||
modalPosition = 0, onPress, onRemove,
|
||||
selectedIds, setShowToast, showToast = false,
|
||||
teammateNameDisplay, toastIcon, toastMessage,
|
||||
teammateNameDisplay, testID, toastIcon, toastMessage,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
|
@ -157,7 +162,7 @@ export default function SelectedUsers({
|
|||
user={selectedIds[id]}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
onRemove={onRemove}
|
||||
testID='create_direct_message.selected_user'
|
||||
testID={`${testID}.selected_user`}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
|
@ -276,6 +281,7 @@ export default function SelectedUsers({
|
|||
icon={buttonIcon}
|
||||
text={buttonText}
|
||||
disabled={numberSelectedIds > General.MAX_USERS_IN_GM}
|
||||
testID={`${testID}.start.button`}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import React, {useCallback, useMemo} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import {github, monokai, solarizedDark, solarizedLight} from 'react-syntax-highlighter/dist/cjs/styles/hljs';
|
||||
import {githubGist as github, monokai, solarizedDark, solarizedLight} from 'react-syntax-highlighter/dist/cjs/styles/hljs';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ function createNativeElement({node, stylesheet, key, defaultColor, fontFamily, f
|
|||
<Text
|
||||
key={key}
|
||||
style={Object.assign({color: defaultColor}, startingStyle)}
|
||||
selectable={true}
|
||||
selectable={selectable}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import React, {useCallback} from 'react';
|
|||
import {ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
import {FlatList} from 'react-native-gesture-handler'; // Keep the FlatList from gesture handler so it works well with bottom sheet
|
||||
|
||||
import Loading from '@components/loading';
|
||||
|
||||
import TeamListItem from './team_list_item';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
|
@ -17,6 +19,8 @@ type Props = {
|
|||
onPress: (id: string) => void;
|
||||
testID?: string;
|
||||
selectedTeamId?: string;
|
||||
onEndReached?: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
@ -30,7 +34,7 @@ const styles = StyleSheet.create({
|
|||
|
||||
const keyExtractor = (item: TeamModel) => item.id;
|
||||
|
||||
export default function TeamList({teams, textColor, iconTextColor, iconBackgroundColor, onPress, testID, selectedTeamId}: Props) {
|
||||
export default function TeamList({teams, textColor, iconTextColor, iconBackgroundColor, onPress, testID, selectedTeamId, onEndReached, loading = false}: Props) {
|
||||
const renderTeam = useCallback(({item: t}: ListRenderItemInfo<Team|TeamModel>) => {
|
||||
return (
|
||||
<TeamListItem
|
||||
|
|
@ -44,6 +48,11 @@ export default function TeamList({teams, textColor, iconTextColor, iconBackgroun
|
|||
);
|
||||
}, [textColor, iconTextColor, iconBackgroundColor, onPress, selectedTeamId]);
|
||||
|
||||
let footer;
|
||||
if (loading) {
|
||||
footer = (<Loading/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
|
|
@ -52,6 +61,8 @@ export default function TeamList({teams, textColor, iconTextColor, iconBackgroun
|
|||
keyExtractor={keyExtractor}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
testID={`${testID}.flat_list`}
|
||||
onEndReached={onEndReached}
|
||||
ListFooterComponent={footer}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {addCurrentUserToTeam, handleTeamChange} from '@actions/remote/team';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Empty from '@components/illustrations/no_team';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import BottomSheetContent from '@screens/bottom_sheet/content';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import TeamList from './team_list';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
empty: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
lineHeight: 28,
|
||||
marginTop: 16,
|
||||
...typography('Heading', 400, 'Regular'),
|
||||
},
|
||||
description: {
|
||||
color: theme.centerChannelColor,
|
||||
marginTop: 8,
|
||||
maxWidth: 334,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
otherTeams: TeamModel[];
|
||||
title: string;
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
export default function AddTeamSlideUp({otherTeams, title, showTitle = true}: Props) {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const onPressCreate = useCallback(() => {
|
||||
//TODO Create team screen https://mattermost.atlassian.net/browse/MM-43622
|
||||
dismissBottomSheet();
|
||||
}, []);
|
||||
|
||||
const onPress = useCallback(async (teamId: string) => {
|
||||
const {error} = await addCurrentUserToTeam(serverUrl, teamId);
|
||||
if (!error) {
|
||||
await dismissBottomSheet();
|
||||
handleTeamChange(serverUrl, teamId);
|
||||
}
|
||||
}, [serverUrl]);
|
||||
|
||||
const hasOtherTeams = Boolean(otherTeams.length);
|
||||
|
||||
return (
|
||||
<BottomSheetContent
|
||||
buttonIcon='plus'
|
||||
buttonText={intl.formatMessage({id: 'mobile.add_team.create_team', defaultMessage: 'Create a new team'})}
|
||||
onPress={onPressCreate}
|
||||
showButton={false}
|
||||
showTitle={showTitle}
|
||||
testID='team_sidebar.add_team_slide_up'
|
||||
title={title}
|
||||
>
|
||||
{hasOtherTeams &&
|
||||
<TeamList
|
||||
teams={otherTeams}
|
||||
onPress={onPress}
|
||||
testID='team_sidebar.add_team_slide_up.team_list'
|
||||
/>
|
||||
}
|
||||
{!hasOtherTeams &&
|
||||
<View style={styles.empty}>
|
||||
<Empty theme={theme}/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.title'
|
||||
defaultMessage='No additional teams to join'
|
||||
style={styles.title}
|
||||
testID='team_sidebar.add_team_slide_up.no_other_teams.title'
|
||||
/>
|
||||
<FormattedText
|
||||
id='team_list.no_other_teams.description'
|
||||
defaultMessage='To join another team, ask a Team Admin for an invitation, or create your own team.'
|
||||
style={styles.description}
|
||||
testID='team_sidebar.add_team_slide_up.no_other_teams.description'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</BottomSheetContent>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,26 +3,16 @@
|
|||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useWindowDimensions, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {getTeamsSnapHeight} from '@utils/team_list';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import AddTeamSlideUp from './add_team_slide_up';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type Props = {
|
||||
otherTeams: TeamModel[];
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
|
|
@ -45,35 +35,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
export default function AddTeam({otherTeams}: Props) {
|
||||
export default function AddTeam() {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const dimensions = useWindowDimensions();
|
||||
const intl = useIntl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const onPress = useCallback(preventDoubleTap(() => {
|
||||
const title = intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'});
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<AddTeamSlideUp
|
||||
otherTeams={otherTeams}
|
||||
showTitle={!isTablet && Boolean(otherTeams.length)}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-join-team';
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: 'close.join_team.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
const height = getTeamsSnapHeight({dimensions, teams: otherTeams, insets});
|
||||
bottomSheet({
|
||||
closeButtonId: 'close-team_list',
|
||||
renderContent,
|
||||
snapPoints: [height, 10],
|
||||
theme,
|
||||
title,
|
||||
});
|
||||
}), [otherTeams, intl, isTablet, dimensions, theme]);
|
||||
showModal(Screens.JOIN_TEAM, title, {closeButtonId}, options);
|
||||
}), [intl]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryMyTeams, queryOtherTeams} from '@queries/servers/team';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
import TeamSidebar from './team_sidebar';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const enhanced = withObservables([], ({serverUrl}: {serverUrl: string}) => {
|
||||
// TODO https://mattermost.atlassian.net/browse/MM-43622
|
||||
// const canCreateTeams = observeCurrentUser(database).pipe(
|
||||
// switchMap((u) => (u ? of$(u.roles.split(' ')) : of$([]))),
|
||||
|
|
@ -19,17 +16,9 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
|||
// switchMap((r) => of$(hasPermission(r, Permissions.CREATE_TEAM, false))),
|
||||
// );
|
||||
|
||||
const otherTeams = queryMyTeams(database).observe().pipe(
|
||||
switchMap((mm) => {
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const ids = mm.map((m) => m.id);
|
||||
return queryOtherTeams(database, ids).observe();
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
otherTeams,
|
||||
canJoinOtherTeams: EphemeralStore.observeCanJoinOtherTeams(serverUrl),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(TeamSidebar));
|
||||
export default withServerUrl(enhanced(TeamSidebar));
|
||||
|
|
|
|||
|
|
@ -11,11 +11,9 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
|
|||
import AddTeam from './add_team';
|
||||
import TeamList from './team_list';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type Props = {
|
||||
iconPad?: boolean;
|
||||
otherTeams: TeamModel[];
|
||||
canJoinOtherTeams: boolean;
|
||||
teamsCount: number;
|
||||
}
|
||||
|
||||
|
|
@ -38,8 +36,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
export default function TeamSidebar({iconPad, otherTeams, teamsCount}: Props) {
|
||||
const showAddTeam = otherTeams.length > 0;
|
||||
export default function TeamSidebar({iconPad, canJoinOtherTeams, teamsCount}: Props) {
|
||||
const initialWidth = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
const width = useSharedValue(initialWidth);
|
||||
const marginTop = useSharedValue(iconPad ? 44 : 0);
|
||||
|
|
@ -68,10 +65,8 @@ export default function TeamSidebar({iconPad, otherTeams, teamsCount}: Props) {
|
|||
<Animated.View style={[styles.container, transform]}>
|
||||
<Animated.View style={[styles.listContainer, serverStyle]}>
|
||||
<TeamList testID='team_sidebar.team_list'/>
|
||||
{showAddTeam && (
|
||||
<AddTeam
|
||||
otherTeams={otherTeams}
|
||||
/>
|
||||
{canJoinOtherTeams && (
|
||||
<AddTeam/>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ const TutorialSwipeLeft = ({containerStyle, message, style, textStyles}: Props)
|
|||
<View
|
||||
pointerEvents='none'
|
||||
style={[styles.container, containerStyle]}
|
||||
testID='tutorial_swipe_left'
|
||||
>
|
||||
<View style={[styles.view, style]}>
|
||||
<LongPressIllustration/>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ const TutorialSwipeLeft = ({containerStyle, message, style, textStyles}: Props)
|
|||
<View
|
||||
pointerEvents='none'
|
||||
style={[styles.container, containerStyle]}
|
||||
testID='tutorial_swipe_left'
|
||||
>
|
||||
<View style={[styles.view, style]}>
|
||||
<HandSwipeLeft fillColor={theme.centerChannelColor}/>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@
|
|||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {StyleProp, Text, TouchableOpacity, View, ViewStyle} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {TITLE_HEIGHT} from '@screens/bottom_sheet/content';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -19,6 +22,7 @@ import UsersList from './users_list';
|
|||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const OVERFLOW_DISPLAY_LIMIT = 99;
|
||||
const USER_ROW_HEIGHT = 40;
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
|
|
@ -88,9 +92,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
marginBottom: 12,
|
||||
},
|
||||
listHeaderText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
textTransform: 'uppercase',
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -99,6 +102,7 @@ const UserAvatarsStack = ({breakAt = 3, channelId, location, style: baseContaine
|
|||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
|
||||
const showParticipantsList = useCallback(preventDoubleTap(() => {
|
||||
const renderContent = () => (
|
||||
|
|
@ -119,15 +123,16 @@ const UserAvatarsStack = ({breakAt = 3, channelId, location, style: baseContaine
|
|||
/>
|
||||
</>
|
||||
);
|
||||
const snap = bottomSheetSnapPoint(Math.min(users.length, 5), USER_ROW_HEIGHT, bottom);
|
||||
bottomSheet({
|
||||
closeButtonId: 'close-set-user-status',
|
||||
renderContent,
|
||||
initialSnapIndex: 1,
|
||||
snapPoints: ['90%', '50%', 10],
|
||||
snapPoints: ['90%', TITLE_HEIGHT + snap, 10],
|
||||
title: intl.formatMessage({id: 'mobile.participants.header', defaultMessage: 'Thread Participants'}),
|
||||
theme,
|
||||
});
|
||||
}), [isTablet, theme, users, channelId, location]);
|
||||
}), [isTablet, theme, users, channelId, location, bottom]);
|
||||
|
||||
const displayUsers = users.slice(0, breakAt);
|
||||
const overflowUsersCount = Math.min(users.length - displayUsers.length, OVERFLOW_DISPLAY_LIMIT);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type AtMentionItemProps = {
|
|||
showFullName: boolean;
|
||||
testID?: string;
|
||||
isCustomStatusEnabled: boolean;
|
||||
pictureContainerStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const getName = (user: UserProfile | UserModel | undefined, showFullName: boolean, isCurrentUser: boolean, intl: IntlShape) => {
|
||||
|
|
@ -94,6 +95,7 @@ const UserItem = ({
|
|||
showFullName,
|
||||
testID,
|
||||
isCustomStatusEnabled,
|
||||
pictureContainerStyle,
|
||||
}: AtMentionItemProps) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
|
@ -124,7 +126,7 @@ const UserItem = ({
|
|||
style={[style.row, containerStyle]}
|
||||
testID={userItemTestId}
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<View style={[style.rowPicture, pictureContainerStyle]}>
|
||||
<ProfilePicture
|
||||
author={user}
|
||||
size={24}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const DeepLinkType = {
|
|||
Invalid: 'invalid',
|
||||
Permalink: 'permalink',
|
||||
Plugin: 'plugin',
|
||||
Redirect: '_redirect',
|
||||
} as const;
|
||||
|
||||
export default DeepLinkType;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ export default keyMirror({
|
|||
CHANNEL_SWITCH: null,
|
||||
CLOSE_BOTTOM_SHEET: null,
|
||||
CONFIG_CHANGED: null,
|
||||
FETCHING_POSTS: null,
|
||||
FREEZE_SCREEN: null,
|
||||
GALLERY_ACTIONS: null,
|
||||
LEAVE_CHANNEL: null,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
const LaunchType = {
|
||||
AddServer: 'add-server',
|
||||
AddServerFromDeepLink: 'add-server-deeplink',
|
||||
Normal: 'normal',
|
||||
DeepLink: 'deeplink',
|
||||
Notification: 'notification',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const DEFAULT_EMOJIS = [
|
|||
];
|
||||
export const LARGE_ICON_SIZE = 32;
|
||||
export const LARGE_CONTAINER_SIZE = 48;
|
||||
export const REACTION_PICKER_HEIGHT = 72;
|
||||
export const REACTION_PICKER_HEIGHT = 48;
|
||||
export const SMALL_CONTAINER_SIZE = 44;
|
||||
export const SMALL_ICON_BREAKPOINT = 410;
|
||||
export const SMALL_ICON_SIZE = 28;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export const INTEGRATION_SELECTOR = 'IntegrationSelector';
|
|||
export const INTERACTIVE_DIALOG = 'InteractiveDialog';
|
||||
export const INVITE = 'Invite';
|
||||
export const IN_APP_NOTIFICATION = 'InAppNotification';
|
||||
export const JOIN_TEAM = 'JoinTeam';
|
||||
export const LATEX = 'Latex';
|
||||
export const LOGIN = 'Login';
|
||||
export const MENTIONS = 'Mentions';
|
||||
|
|
@ -96,6 +97,7 @@ export default {
|
|||
INTERACTIVE_DIALOG,
|
||||
INVITE,
|
||||
IN_APP_NOTIFICATION,
|
||||
JOIN_TEAM,
|
||||
LATEX,
|
||||
LOGIN,
|
||||
MENTIONS,
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ export default {
|
|||
TOWN_SQUARE_READ_ONLY_ERROR: 'api.post.create_post.town_square_read_only',
|
||||
PLUGIN_DISMISSED_POST_ERROR: 'plugin.message_will_be_posted.dismiss_post',
|
||||
SEND_EMAIL_WITH_DEFAULTS_ERROR: 'api.team.invite_members.unable_to_send_email_with_defaults.app_error',
|
||||
TEAM_MEMBERSHIP_DENIAL_ERROR_ID: 'api.team.add_members.user_denied',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ const WebsocketEvents = {
|
|||
THREAD_UPDATED: 'thread_updated',
|
||||
THREAD_FOLLOW_CHANGED: 'thread_follow_changed',
|
||||
THREAD_READ_CHANGED: 'thread_read_changed',
|
||||
DELETE_TEAM: 'delete_team',
|
||||
RESTORE_TEAM: 'restore_team',
|
||||
APPS_FRAMEWORK_REFRESH_BINDINGS: 'custom_com.mattermost.apps_refresh_bindings',
|
||||
CALLS_CHANNEL_ENABLED: `custom_${Calls.PluginId}_channel_enable_voice`,
|
||||
CALLS_CHANNEL_DISABLED: `custom_${Calls.PluginId}_channel_disable_voice`,
|
||||
|
|
|
|||
|
|
@ -104,11 +104,18 @@ export default class CategoryModel extends Model implements CategoryInterface {
|
|||
Q.sortBy('last_post_at', Q.desc),
|
||||
);
|
||||
|
||||
/** hasChannels : Returns a boolean indicating if the category has channels */
|
||||
@lazy hasChannels = this.categoryChannels.observeCount().pipe(
|
||||
map((c) => c > 0),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
observeHasChannels = (canViewArchived: boolean, channelId: string) => {
|
||||
return this.channels.observeWithColumns(['delete_at']).pipe(
|
||||
map((channels) => {
|
||||
if (canViewArchived) {
|
||||
return channels.filter((c) => c.deleteAt === 0 || c.id === channelId).length > 0;
|
||||
}
|
||||
|
||||
return channels.filter((c) => c.deleteAt === 0).length > 0;
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
toCategoryWithChannels = async (): Promise<CategoryWithChannels> => {
|
||||
const categoryChannels = await this.categoryChannels.fetch();
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ export const transformUserRecord = ({action, database, value}: TransformerArgs):
|
|||
user.timezone = raw.timezone || null;
|
||||
user.isBot = raw.is_bot ?? false;
|
||||
user.remoteId = raw?.remote_id ?? null;
|
||||
user.termsOfServiceId = raw?.terms_of_service_id ?? '';
|
||||
user.termsOfServiceCreateAt = raw?.terms_of_service_create_at ?? 0;
|
||||
user.termsOfServiceId = raw?.terms_of_service_id ?? (record?.termsOfServiceId || '');
|
||||
user.termsOfServiceCreateAt = raw?.terms_of_service_create_at ?? (record?.termsOfServiceCreateAt || 0);
|
||||
if (raw.status) {
|
||||
user.status = raw.status;
|
||||
}
|
||||
|
|
|
|||
22
app/hooks/fetching_thread.ts
Normal file
22
app/hooks/fetching_thread.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {subject} from '@store/fetching_thread_store';
|
||||
|
||||
export const useFetchingThreadState = (rootId: string) => {
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
useEffect(() => {
|
||||
const sub = subject.pipe(
|
||||
switchMap((s) => of$(s[rootId] || false)),
|
||||
distinctUntilChanged(),
|
||||
).subscribe(setIsFetching);
|
||||
|
||||
return () => sub.unsubscribe();
|
||||
}, []);
|
||||
|
||||
return isFetching;
|
||||
};
|
||||
|
|
@ -24,10 +24,13 @@ export const useKeyboardTrackingPaused = (keyboardTrackingRef: RefObject<Keyboar
|
|||
};
|
||||
|
||||
const commandListener = Navigation.events().registerCommandListener(() => {
|
||||
if (!isPostDraftPaused.current) {
|
||||
isPostDraftPaused.current = true;
|
||||
keyboardTrackingRef.current?.pauseTracking(trackerId);
|
||||
}
|
||||
setTimeout(() => {
|
||||
const visibleScreen = NavigationStore.getVisibleScreen();
|
||||
if (!isPostDraftPaused.current && !screens.includes(visibleScreen)) {
|
||||
isPostDraftPaused.current = true;
|
||||
keyboardTrackingRef.current?.pauseTracking(trackerId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const commandCompletedListener = Navigation.events().registerCommandCompletedListener(() => {
|
||||
|
|
|
|||
24
app/hooks/teams_loading.ts
Normal file
24
app/hooks/teams_loading.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {getLoadingTeamChannelsSubject} from '@store/team_load_store';
|
||||
|
||||
export const useTeamsLoading = (serverUrl: string) => {
|
||||
// const subject = getLoadingTeamChannelsSubject(serverUrl);
|
||||
// const [loading, setLoading] = useState(subject.getValue() !== 0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
const sub = getLoadingTeamChannelsSubject(serverUrl).pipe(
|
||||
switchMap((v) => of$(v !== 0)),
|
||||
distinctUntilChanged(),
|
||||
).subscribe(setLoading);
|
||||
|
||||
return () => sub.unsubscribe();
|
||||
}, []);
|
||||
|
||||
return loading;
|
||||
};
|
||||
|
|
@ -7,20 +7,20 @@ import {Notifications} from 'react-native-notifications';
|
|||
|
||||
import {appEntry, pushNotificationEntry, upgradeEntry} from '@actions/remote/entry';
|
||||
import LocalConfig from '@assets/config.json';
|
||||
import {Screens, DeepLink, Events, Launch, PushNotification} from '@constants';
|
||||
import {DeepLink, Events, Launch, PushNotification} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getActiveServerUrl, getServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {getOnboardingViewed} from '@queries/app/global';
|
||||
import {getThemeForCurrentTeam} from '@queries/servers/preference';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import {goToScreen, resetToHome, resetToSelectServer, resetToTeams, resetToOnboarding} from '@screens/navigation';
|
||||
import {resetToHome, resetToSelectServer, resetToTeams, resetToOnboarding} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {getLaunchPropsFromDeepLink} from '@utils/deep_link';
|
||||
import {logInfo} from '@utils/log';
|
||||
import {convertToNotificationData} from '@utils/notification';
|
||||
import {parseDeepLink} from '@utils/url';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch';
|
||||
import type {DeepLinkWithData, LaunchProps} from '@typings/launch';
|
||||
|
||||
const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, PushNotification.NOTIFICATION_TYPE.SESSION];
|
||||
|
||||
|
|
@ -67,13 +67,18 @@ const launchAppFromNotification = async (notification: NotificationWithData, col
|
|||
|
||||
* @returns a redirection to a screen, either onboarding, add_server, login or home depending on the scenario
|
||||
*/
|
||||
const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
||||
const launchApp = async (props: LaunchProps) => {
|
||||
let serverUrl: string | undefined;
|
||||
switch (props?.launchType) {
|
||||
case Launch.DeepLink:
|
||||
if (props.extra?.type !== DeepLink.Invalid) {
|
||||
const extra = props.extra as DeepLinkWithData;
|
||||
serverUrl = extra.data?.serverUrl;
|
||||
const existingServer = DatabaseManager.searchUrl(extra.data!.serverUrl);
|
||||
serverUrl = existingServer;
|
||||
props.serverUrl = serverUrl || extra.data?.serverUrl;
|
||||
if (!serverUrl) {
|
||||
props.launchError = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Launch.Notification: {
|
||||
|
|
@ -142,17 +147,17 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
|||
return resetToOnboarding(props);
|
||||
}
|
||||
|
||||
return launchToServer(props, resetNavigation);
|
||||
return resetToSelectServer(props);
|
||||
};
|
||||
|
||||
const launchToHome = async (props: LaunchProps) => {
|
||||
let openPushNotification = false;
|
||||
|
||||
switch (props.launchType) {
|
||||
case Launch.DeepLink:
|
||||
// TODO:
|
||||
// deepLinkEntry({props.serverUrl, props.extra});
|
||||
case Launch.DeepLink: {
|
||||
appEntry(props.serverUrl!);
|
||||
break;
|
||||
}
|
||||
case Launch.Notification: {
|
||||
const extra = props.extra as NotificationWithData;
|
||||
openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local);
|
||||
|
|
@ -185,55 +190,8 @@ const launchToHome = async (props: LaunchProps) => {
|
|||
return resetToTeams();
|
||||
};
|
||||
|
||||
const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => {
|
||||
if (resetNavigation) {
|
||||
return resetToSelectServer(props);
|
||||
}
|
||||
|
||||
// This is being called for Deeplinks, but needs to be revisited when
|
||||
// the implementation of deep links is complete
|
||||
const title = '';
|
||||
return goToScreen(Screens.SERVER, title, {...props});
|
||||
};
|
||||
|
||||
export const relaunchApp = (props: LaunchProps, resetNavigation = false) => {
|
||||
return launchApp(props, resetNavigation);
|
||||
};
|
||||
|
||||
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = false): LaunchProps => {
|
||||
const parsed = parseDeepLink(deepLinkUrl);
|
||||
const launchProps: LaunchProps = {
|
||||
launchType: Launch.DeepLink,
|
||||
coldStart,
|
||||
};
|
||||
|
||||
switch (parsed.type) {
|
||||
case DeepLink.Invalid:
|
||||
launchProps.launchError = true;
|
||||
break;
|
||||
case DeepLink.Channel: {
|
||||
const parsedData = parsed.data as DeepLinkChannel;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
case DeepLink.DirectMessage: {
|
||||
const parsedData = parsed.data as DeepLinkDM;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
case DeepLink.GroupMessage: {
|
||||
const parsedData = parsed.data as DeepLinkGM;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
case DeepLink.Permalink: {
|
||||
const parsedData = parsed.data as DeepLinkPermalink;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return launchProps;
|
||||
export const relaunchApp = (props: LaunchProps) => {
|
||||
return launchApp(props);
|
||||
};
|
||||
|
||||
export const getLaunchPropsFromNotification = async (notification: NotificationWithData, coldStart = false): Promise<LaunchProps> => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AppState, DeviceEventEmitter, Platform} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {
|
||||
Notification,
|
||||
NotificationAction,
|
||||
|
|
@ -29,6 +28,7 @@ import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread';
|
|||
import {dismissOverlay, showOverlay} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isBetaApp} from '@utils/general';
|
||||
import {isMainActivity, isTablet} from '@utils/helpers';
|
||||
import {logInfo} from '@utils/log';
|
||||
import {convertToNotificationData} from '@utils/notification';
|
||||
|
|
@ -248,7 +248,7 @@ class PushNotifications {
|
|||
|
||||
if (Platform.OS === 'ios') {
|
||||
prefix = Device.PUSH_NOTIFY_APPLE_REACT_NATIVE;
|
||||
if (DeviceInfo.getBundleId().includes('rnbeta')) {
|
||||
if (isBetaApp) {
|
||||
prefix = `${prefix}beta`;
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import {Events, Sso} from '@constants';
|
|||
import {MIN_REQUIRED_VERSION} from '@constants/supported_server';
|
||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
|
||||
import * as analytics from '@managers/analytics';
|
||||
import {getAllServers} from '@queries/app/servers';
|
||||
import {handleDeepLink} from '@utils/deep_link';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type {jsAndNativeErrorHandler} from '@typings/global/error_handling';
|
||||
|
|
@ -64,8 +64,7 @@ class GlobalEventHandler {
|
|||
}
|
||||
|
||||
if (event.url) {
|
||||
const props = getLaunchPropsFromDeepLink(event.url);
|
||||
relaunchApp(props);
|
||||
handleDeepLink(event.url);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ class SessionManager {
|
|||
await storeOnboardingViewedValue(false);
|
||||
}
|
||||
|
||||
relaunchApp({launchType, serverUrl, displayName}, true);
|
||||
relaunchApp({launchType, serverUrl, displayName});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ class SessionManager {
|
|||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const serverDisplayName = await getServerDisplayName(serverUrl);
|
||||
|
||||
await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName}, true);
|
||||
await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName});
|
||||
if (activeServerUrl) {
|
||||
addNewServer(getThemeFromState(), serverUrl, serverDisplayName);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import {isMainActivity} from '@utils/helpers';
|
|||
import {logError} from '@utils/log';
|
||||
|
||||
const WAIT_TO_CLOSE = toMilliseconds({seconds: 15});
|
||||
const WAIT_UNTIL_NEXT = toMilliseconds({seconds: 20});
|
||||
const WAIT_UNTIL_NEXT = toMilliseconds({seconds: 5});
|
||||
|
||||
class WebsocketManager {
|
||||
private connectedSubjects: {[serverUrl: string]: BehaviorSubject<boolean>} = {};
|
||||
private connectedSubjects: {[serverUrl: string]: BehaviorSubject<WebsocketConnectedState>} = {};
|
||||
|
||||
private clients: Record<string, WebSocketClient> = {};
|
||||
private connectionTimerIDs: Record<string, DebouncedFunc<() => void>> = {};
|
||||
|
|
@ -69,7 +69,7 @@ class WebsocketManager {
|
|||
}
|
||||
delete this.clients[serverUrl];
|
||||
|
||||
this.getConnectedSubject(serverUrl).next(false);
|
||||
this.getConnectedSubject(serverUrl).next('not_connected');
|
||||
delete this.connectedSubjects[serverUrl];
|
||||
};
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ class WebsocketManager {
|
|||
const client = this.clients[url];
|
||||
if (client.isConnected()) {
|
||||
client.close(true);
|
||||
this.getConnectedSubject(url).next(false);
|
||||
this.getConnectedSubject(url).next('not_connected');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -107,6 +107,7 @@ class WebsocketManager {
|
|||
if (clientUrl === activeServerUrl) {
|
||||
this.initializeClient(clientUrl);
|
||||
} else {
|
||||
this.getConnectedSubject(clientUrl).next('connecting');
|
||||
const bounce = debounce(this.initializeClient.bind(this, clientUrl), WAIT_UNTIL_NEXT);
|
||||
this.connectionTimerIDs[clientUrl] = bounce;
|
||||
bounce();
|
||||
|
|
@ -118,7 +119,7 @@ class WebsocketManager {
|
|||
return this.clients[serverUrl]?.isConnected();
|
||||
};
|
||||
|
||||
public observeConnected = (serverUrl: string) => {
|
||||
public observeWebsocketState = (serverUrl: string) => {
|
||||
return this.getConnectedSubject(serverUrl).asObservable().pipe(
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
|
@ -126,7 +127,7 @@ class WebsocketManager {
|
|||
|
||||
private getConnectedSubject = (serverUrl: string) => {
|
||||
if (!this.connectedSubjects[serverUrl]) {
|
||||
this.connectedSubjects[serverUrl] = new BehaviorSubject(this.isConnected(serverUrl));
|
||||
this.connectedSubjects[serverUrl] = new BehaviorSubject(this.isConnected(serverUrl) ? 'connected' : 'not_connected');
|
||||
}
|
||||
|
||||
return this.connectedSubjects[serverUrl];
|
||||
|
|
@ -153,13 +154,13 @@ class WebsocketManager {
|
|||
private onFirstConnect = (serverUrl: string) => {
|
||||
this.startPeriodicStatusUpdates(serverUrl);
|
||||
handleFirstConnect(serverUrl);
|
||||
this.getConnectedSubject(serverUrl).next(true);
|
||||
this.getConnectedSubject(serverUrl).next('connected');
|
||||
};
|
||||
|
||||
private onReconnect = (serverUrl: string) => {
|
||||
private onReconnect = async (serverUrl: string) => {
|
||||
this.startPeriodicStatusUpdates(serverUrl);
|
||||
handleReconnect(serverUrl);
|
||||
this.getConnectedSubject(serverUrl).next(true);
|
||||
await handleReconnect(serverUrl);
|
||||
this.getConnectedSubject(serverUrl).next('connected');
|
||||
};
|
||||
|
||||
private onWebsocketClose = async (serverUrl: string, connectFailCount: number, lastDisconnect: number) => {
|
||||
|
|
@ -168,7 +169,7 @@ class WebsocketManager {
|
|||
await handleClose(serverUrl, lastDisconnect);
|
||||
|
||||
this.stopPeriodicStatusUpdates(serverUrl);
|
||||
this.getConnectedSubject(serverUrl).next(false);
|
||||
this.getConnectedSubject(serverUrl).next('not_connected');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const ChannelInfoStartButton = ({
|
|||
destructiveText={leaveText}
|
||||
destructiveIconName={'phone-hangup'}
|
||||
isDestructive={alreadyInCall}
|
||||
testID='channel_info.options.join_start_call.option'
|
||||
testID='channel_info.channel_actions.join_start_call.action'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ const CallScreen = ({
|
|||
const callThreadOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Call Thread'});
|
||||
const recordOptionTitle = intl.formatMessage({id: 'mobile.calls_record', defaultMessage: 'Record'});
|
||||
const stopRecordingOptionTitle = intl.formatMessage({id: 'mobile.calls_stop_recording', defaultMessage: 'Stop Recording'});
|
||||
const openChannelOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Open Channel'});
|
||||
const openChannelOptionTitle = intl.formatMessage({id: 'mobile.calls_open_channel', defaultMessage: 'Open Channel'});
|
||||
|
||||
useEffect(() => {
|
||||
mergeNavigationOptions('Call', {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {FAVORITES_CATEGORY} from '@constants/categories';
|
|||
import {MM_TABLES} from '@constants/database';
|
||||
import {makeCategoryChannelId} from '@utils/categories';
|
||||
import {pluckUnique} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import {observeChannelsByLastPostAt} from './channel';
|
||||
|
||||
|
|
@ -38,16 +39,10 @@ export const queryCategoriesByTeamIds = (database: Database, teamIds: string[])
|
|||
|
||||
export async function prepareCategoriesAndCategoriesChannels(operator: ServerDataOperator, categories: CategoryWithChannels[], prune = false) {
|
||||
try {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const preparedCategories = prepareCategories(operator, categories);
|
||||
if (preparedCategories) {
|
||||
modelPromises.push(preparedCategories);
|
||||
}
|
||||
|
||||
const preparedCategoryChannels = prepareCategoryChannels(operator, categories);
|
||||
if (preparedCategoryChannels) {
|
||||
modelPromises.push(preparedCategoryChannels);
|
||||
}
|
||||
const modelPromises: Array<Promise<Model[]>> = [
|
||||
prepareCategories(operator, categories),
|
||||
prepareCategoryChannels(operator, categories),
|
||||
];
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
const flattenedModels = models.flat();
|
||||
|
|
@ -71,7 +66,8 @@ export async function prepareCategoriesAndCategoriesChannels(operator: ServerDat
|
|||
}
|
||||
|
||||
return flattenedModels;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
logDebug('error while preparing categories and categories channels', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ export const queryPinnedPostsInChannel = (database: Database, channelId: string)
|
|||
Q.where('channel_id', channelId),
|
||||
Q.where('is_pinned', Q.eq(true)),
|
||||
),
|
||||
Q.sortBy('create_at', Q.asc),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -404,7 +404,13 @@ export async function setCurrentTeamAndChannelId(operator: ServerDataOperator, t
|
|||
export const observeLastUnreadChannelId = (database: Database): Observable<string> => {
|
||||
return querySystemValue(database, SYSTEM_IDENTIFIERS.LAST_UNREAD_CHANNEL_ID).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$({value: ''}))),
|
||||
switchMap((model) => of$(model.value)),
|
||||
switchMap((model) => {
|
||||
if (model.value) {
|
||||
return of$(model.value);
|
||||
}
|
||||
|
||||
return observeCurrentChannelId(database);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -165,31 +165,6 @@ export const getLastTeam = async (database: Database, ignoreIdForDefault?: strin
|
|||
return getDefaultTeamId(database, ignoreIdForDefault);
|
||||
};
|
||||
|
||||
export async function syncTeamTable(operator: ServerDataOperator, teams: Team[]) {
|
||||
try {
|
||||
const deletedTeams = teams.filter((t) => t.delete_at > 0).map((t) => t.id);
|
||||
const deletedSet = new Set(deletedTeams);
|
||||
const availableTeams = teams.filter((a) => !deletedSet.has(a.id));
|
||||
const models = [];
|
||||
|
||||
if (deletedTeams.length) {
|
||||
const notAvailable = await operator.database.get<TeamModel>(TEAM).query(Q.where('id', Q.oneOf(deletedTeams))).fetch();
|
||||
const deletions = await Promise.all(notAvailable.map((t) => prepareDeleteTeam(t)));
|
||||
for (const d of deletions) {
|
||||
models.push(...d);
|
||||
}
|
||||
}
|
||||
|
||||
models.push(...await operator.handleTeam({teams: availableTeams, prepareRecordsOnly: true}));
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export const getDefaultTeamId = async (database: Database, ignoreId?: string) => {
|
||||
const user = await getCurrentUser(database);
|
||||
const config = await getConfig(database);
|
||||
|
|
|
|||
43
app/queries/servers/terms_of_service.ts
Normal file
43
app/queries/servers/terms_of_service.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import {of as of$, combineLatest} from 'rxjs';
|
||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {observeLicense, observeConfigBooleanValue, observeConfigValue, observeConfigIntValue} from './system';
|
||||
import {observeCurrentUser} from './user';
|
||||
|
||||
export const observeShowToS = (database: Database) => {
|
||||
const isLicensed = observeLicense(database).pipe(
|
||||
switchMap((lcs) => (lcs ? of$(lcs.IsLicensed === 'true') : of$(false))),
|
||||
);
|
||||
const currentUser = observeCurrentUser(database);
|
||||
const customTermsOfServiceEnabled = observeConfigBooleanValue(database, 'EnableCustomTermsOfService');
|
||||
const customTermsOfServiceId = observeConfigValue(database, 'CustomTermsOfServiceId');
|
||||
const customTermsOfServicePeriod = observeConfigIntValue(database, 'CustomTermsOfServiceReAcceptancePeriod');
|
||||
|
||||
const showToS = combineLatest([
|
||||
isLicensed,
|
||||
customTermsOfServiceEnabled,
|
||||
currentUser,
|
||||
customTermsOfServiceId,
|
||||
customTermsOfServicePeriod,
|
||||
]).pipe(
|
||||
switchMap(([lcs, cfg, user, id, period]) => {
|
||||
if (!lcs || !cfg) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
if (user?.termsOfServiceId !== id) {
|
||||
return of$(true);
|
||||
}
|
||||
|
||||
const timeElapsed = Date.now() - (user?.termsOfServiceCreateAt || 0);
|
||||
return of$(timeElapsed > (period * 24 * 60 * 60 * 1000));
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
return showToS;
|
||||
};
|
||||
|
|
@ -141,7 +141,10 @@ export const prepareThreadsFromReceivedPosts = async (operator: ServerDataOperat
|
|||
};
|
||||
|
||||
export const queryThreadsInTeam = (database: Database, teamId: string, onlyUnreads?: boolean, hasReplies?: boolean, isFollowing?: boolean, sort?: boolean, earliest?: number): Query<ThreadModel> => {
|
||||
const query: Q.Clause[] = [];
|
||||
const query: Q.Clause[] = [
|
||||
Q.experimentalNestedJoin(POST, CHANNEL),
|
||||
Q.on(POST, Q.on(CHANNEL, Q.where('delete_at', 0))),
|
||||
];
|
||||
|
||||
if (isFollowing) {
|
||||
query.push(Q.where('is_following', true));
|
||||
|
|
@ -189,30 +192,34 @@ export const queryThreads = (database: Database, teamId?: string, onlyUnreads =
|
|||
Q.where('reply_count', Q.gt(0)),
|
||||
];
|
||||
|
||||
// Only get threads from available channel
|
||||
const channelCondition: Q.Condition[] = [
|
||||
Q.where('delete_at', 0),
|
||||
];
|
||||
|
||||
// If teamId is specified, only get threads in that team
|
||||
if (teamId) {
|
||||
let condition: Q.Condition = Q.where('team_id', teamId);
|
||||
|
||||
if (includeDmGm) {
|
||||
condition = Q.or(
|
||||
Q.where('team_id', teamId),
|
||||
Q.where('team_id', ''),
|
||||
channelCondition.push(
|
||||
Q.or(
|
||||
Q.where('team_id', teamId),
|
||||
Q.where('team_id', ''),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
channelCondition.push(Q.where('team_id', teamId));
|
||||
}
|
||||
|
||||
query.push(
|
||||
Q.experimentalNestedJoin(POST, CHANNEL),
|
||||
Q.on(POST, Q.on(CHANNEL, condition)),
|
||||
);
|
||||
} else if (!includeDmGm) {
|
||||
// fetching all threads from all teams
|
||||
// excluding DM/GM channels
|
||||
query.push(
|
||||
Q.experimentalNestedJoin(POST, CHANNEL),
|
||||
Q.on(POST, Q.on(CHANNEL, Q.where('team_id', Q.notEq('')))),
|
||||
);
|
||||
channelCondition.push(Q.where('team_id', Q.notEq('')));
|
||||
}
|
||||
|
||||
query.push(
|
||||
Q.experimentalNestedJoin(POST, CHANNEL),
|
||||
Q.on(POST, Q.on(CHANNEL, Q.and(...channelCondition))),
|
||||
);
|
||||
|
||||
if (onlyUnreads) {
|
||||
query.push(Q.where('unread_replies', Q.gt(0)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import {combineLatest, Observable, of as of$} from 'rxjs';
|
||||
import {combineLatest, of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {observeMyChannel} from '@queries/servers/channel';
|
||||
import {isChannelAdmin} from '@utils/user';
|
||||
|
||||
import {queryPreferencesByCategoryAndName} from './preference';
|
||||
import {observeCurrentUserId, observeLicense, getCurrentUserId, getConfig, getLicense, observeConfigValue} from './system';
|
||||
|
|
@ -106,24 +104,17 @@ export const observeUserIsTeamAdmin = (database: Database, userId: string, teamI
|
|||
const id = `${teamId}-${userId}`;
|
||||
return database.get<TeamMembershipModel>(TEAM_MEMBERSHIP).query(
|
||||
Q.where('id', Q.eq(id)),
|
||||
).observe().pipe(
|
||||
).observeWithColumns(['scheme_admin']).pipe(
|
||||
switchMap((tm) => of$(tm.length ? tm[0].schemeAdmin : false)),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeUserIsChannelAdmin = (database: Database, userId: string, channelId: string) => {
|
||||
const id = `${channelId}-${userId}`;
|
||||
const myChannelRoles = observeMyChannel(database, channelId).pipe(
|
||||
switchMap((mc) => of$(mc?.roles || '')),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const channelSchemeAdmin = database.get<ChannelMembershipModel>(CHANNEL_MEMBERSHIP).query(
|
||||
return database.get<ChannelMembershipModel>(CHANNEL_MEMBERSHIP).query(
|
||||
Q.where('id', Q.eq(id)),
|
||||
).observe().pipe(
|
||||
).observeWithColumns(['scheme_admin']).pipe(
|
||||
switchMap((cm) => of$(cm.length ? cm[0].schemeAdmin : false)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
return combineLatest([myChannelRoles, channelSchemeAdmin]).pipe(
|
||||
switchMap(([mcr, csa]) => of$(isChannelAdmin(mcr) || csa)),
|
||||
) as Observable<boolean>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ const styles = StyleSheet.create({
|
|||
icon_container: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
marginTop: 2,
|
||||
marginRight: 8,
|
||||
top: -1,
|
||||
marginRight: 4,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -45,9 +45,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
right: 16,
|
||||
right: 20,
|
||||
borderTopWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
marginBottom: 20,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -83,7 +84,7 @@ const BottomSheetContent = ({buttonText, buttonIcon, children, disableButton, on
|
|||
</>
|
||||
{showButton && (
|
||||
<>
|
||||
<View style={[styles.separator, {width: separatorWidth, marginBottom: (isTablet ? TITLE_SEPARATOR_MARGIN_TABLET : TITLE_SEPARATOR_MARGIN)}]}/>
|
||||
<View style={[styles.separator, {width: separatorWidth}]}/>
|
||||
<Button
|
||||
disabled={disableButton}
|
||||
onPress={onPress}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ type SlideUpPanelProps = {
|
|||
}
|
||||
|
||||
export const PADDING_TOP_MOBILE = 20;
|
||||
export const PADDING_TOP_TABLET = 8;
|
||||
|
||||
const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderContent, snapPoints = ['90%', '50%', 50], testID}: SlideUpPanelProps) => {
|
||||
const sheetRef = useRef<RNBottomSheet>(null);
|
||||
|
|
@ -100,7 +101,7 @@ const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderCo
|
|||
|
||||
const backdropStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(backdropOpacity.value, {duration: 250, easing: Easing.inOut(Easing.linear)}),
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
}));
|
||||
|
||||
const renderBackdrop = () => {
|
||||
|
|
@ -126,9 +127,11 @@ const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderCo
|
|||
<View
|
||||
style={{
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
borderWidth: isTablet ? 0 : 1,
|
||||
opacity: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: isTablet ? 0 : PADDING_TOP_MOBILE,
|
||||
paddingTop: isTablet ? PADDING_TOP_TABLET : PADDING_TOP_MOBILE,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
alignSelf: 'center',
|
||||
|
|
@ -154,7 +157,7 @@ const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderCo
|
|||
<RNBottomSheet
|
||||
ref={sheetRef}
|
||||
snapPoints={snapPoints}
|
||||
borderRadius={10}
|
||||
borderRadius={12}
|
||||
initialSnap={snapPoints.length - 1}
|
||||
renderContent={renderContainerContent}
|
||||
onCloseEnd={handleCloseEnd}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
// import AddPeopleBox from '@components/channel_actions/add_people_box';
|
||||
import FavoriteBox from '@components/channel_actions/favorite_box';
|
||||
import InfoBox from '@components/channel_actions/info_box';
|
||||
import SetHeaderBox from '@components/channel_actions/set_header_box';
|
||||
|
|
@ -39,9 +39,10 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const IntroOptions = ({channelId, header, favorite, people}: Props) => {
|
||||
const IntroOptions = ({channelId, header, favorite}: Props) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Add back in after MM-47655 is resolved. https://mattermost.atlassian.net/browse/MM-47655
|
||||
{people &&
|
||||
<AddPeopleBox
|
||||
channelId={channelId}
|
||||
|
|
@ -49,6 +50,7 @@ const IntroOptions = ({channelId, header, favorite, people}: Props) => {
|
|||
testID='channel_post_list.intro_options.add_people.action'
|
||||
/>
|
||||
}
|
||||
*/}
|
||||
{header &&
|
||||
<SetHeaderBox
|
||||
channelId={channelId}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ import {isTypeDMorGM} from '@utils/channel';
|
|||
|
||||
import EditChannel from './edit_channel';
|
||||
import IgnoreMentions from './ignore_mentions';
|
||||
import Members from './members';
|
||||
import PinnedMessages from './pinned_messages';
|
||||
|
||||
// import Members from './members';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
type?: ChannelType;
|
||||
|
|
@ -28,11 +29,16 @@ const Options = ({channelId, type, callsEnabled}: Props) => {
|
|||
}
|
||||
{/*<NotificationPreference channelId={channelId}/>*/}
|
||||
<PinnedMessages channelId={channelId}/>
|
||||
{/* Add back in after MM-47653 is resolved. https://mattermost.atlassian.net/browse/MM-47653
|
||||
{type !== General.DM_CHANNEL &&
|
||||
<Members channelId={channelId}/>
|
||||
}
|
||||
*/}
|
||||
{callsEnabled && !isDMorGM && // if calls is not enabled, copy link will show in the channel actions
|
||||
<CopyChannelLinkOption channelId={channelId}/>
|
||||
<CopyChannelLinkOption
|
||||
channelId={channelId}
|
||||
testID='channel_info.options.copy_channel_link.option'
|
||||
/>
|
||||
}
|
||||
{type !== General.DM_CHANNEL && type !== General.GM_CHANNEL &&
|
||||
<EditChannel channelId={channelId}/>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const Code = ({code, language, textStyle}: Props) => {
|
|||
<SyntaxHiglight
|
||||
code={code}
|
||||
language={language}
|
||||
selectable={managedConfig.copyAndPasteProtection !== 'false'}
|
||||
selectable={managedConfig.copyAndPasteProtection !== 'true'}
|
||||
textStyle={textStyle}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
|
|
|
|||
|
|
@ -396,6 +396,7 @@ export default function CreateDirectMessage({
|
|||
onPress={startConversation}
|
||||
buttonIcon={'forum-outline'}
|
||||
buttonText={formatMessage(messages.buttonText)}
|
||||
testID='create_direct_message'
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
const CustomStatusInput = ({emoji, isStatusSet, onChangeText, onClearHandle, onOpenEmojiPicker, text, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
const placeholder = intl.formatMessage({id: 'custom_status.set_status', defaultMessage: 'Set a Status'});
|
||||
const placeholder = intl.formatMessage({id: 'custom_status.set_status', defaultMessage: 'Set a custom status'});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue