* Revert "Channel Bookmarks (#7750)"
This reverts commit 2c5c878829.
This commit is contained in:
parent
9d3823bb4b
commit
3653df354d
113 changed files with 866 additions and 4476 deletions
|
|
@ -85,7 +85,3 @@ export const storePushDisabledInServerAcknowledged = async (serverUrl: string) =
|
|||
export const removePushDisabledInServerAcknowledged = async (serverUrl: string) => {
|
||||
return storeGlobal(`${GLOBAL_IDENTIFIERS.PUSH_DISABLED_ACK}${serverUrl}`, null, false);
|
||||
};
|
||||
|
||||
export const setAppInactiveSince = async (time: number) => {
|
||||
return storeGlobal(GLOBAL_IDENTIFIERS.APP_INACTIVE_SINCE, time, false);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
async function handleBookmarks(serverUrl: string, bookmarks: ChannelBookmarkWithFileInfo[], prepareRecordsOnly = false) {
|
||||
if (!bookmarks.length) {
|
||||
return {models: undefined};
|
||||
}
|
||||
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const myChannel = await getMyChannel(database, bookmarks[0].channel_id);
|
||||
if (!myChannel) {
|
||||
return {models: undefined};
|
||||
}
|
||||
|
||||
const models = await operator.handleChannelBookmark({bookmarks, prepareRecordsOnly});
|
||||
return {models};
|
||||
}
|
||||
|
||||
export async function handleBookmarkAddedOrDeleted(serverUrl: string, msg: WebSocketMessage<any>, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const bookmark: ChannelBookmarkWithFileInfo = JSON.parse(msg.data.bookmark);
|
||||
return handleBookmarks(serverUrl, [bookmark], prepareRecordsOnly);
|
||||
} catch (error) {
|
||||
logError('cannot handle bookmark added websocket event', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBookmarkEdited(serverUrl: string, msg: WebSocketMessage<any>, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const edited: UpdateChannelBookmarkResponse = JSON.parse(msg.data.bookmarks);
|
||||
const bookmarks = [edited.updated];
|
||||
if (edited.deleted) {
|
||||
bookmarks.push(edited.deleted);
|
||||
}
|
||||
return handleBookmarks(serverUrl, bookmarks, prepareRecordsOnly);
|
||||
} catch (error) {
|
||||
logError('cannot handle bookmark updated websocket event', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBookmarkSorted(serverUrl: string, msg: WebSocketMessage<any>, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const bookmarks: ChannelBookmarkWithFileInfo[] = JSON.parse(msg.data.bookmarks);
|
||||
return handleBookmarks(serverUrl, bookmarks, prepareRecordsOnly);
|
||||
} catch (error) {
|
||||
logError('cannot handle bookmark sorted websocket event', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
@ -34,12 +34,3 @@ export const updateLocalFilePath = async (serverUrl: string, fileId: string, loc
|
|||
}
|
||||
};
|
||||
|
||||
export const getLocalFileInfo = async (serverUrl: string, fileId: string) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const file = await getFileById(database, fileId);
|
||||
return {file};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import {logDebug, logError, logInfo} from '@utils/log';
|
|||
import {showMuteChannelSnackbar} from '@utils/snack_bar';
|
||||
import {displayGroupMessageName, displayUsername} from '@utils/user';
|
||||
|
||||
import {fetchChannelBookmarks} from './channel_bookmark';
|
||||
import {fetchGroupsForChannelIfConstrained} from './groups';
|
||||
import {fetchPostsForChannel} from './post';
|
||||
import {openChannelIfNeeded, savePreference} from './preference';
|
||||
|
|
@ -99,7 +98,6 @@ export async function fetchChannelMembersByIds(serverUrl: string, channelId: str
|
|||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateChannelMemberSchemeRoles(serverUrl: string, channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean, fetchOnly = false) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
|
@ -1040,7 +1038,6 @@ export async function switchToChannelById(serverUrl: string, channelId: string,
|
|||
DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, true);
|
||||
|
||||
fetchPostsForChannel(serverUrl, channelId);
|
||||
fetchChannelBookmarks(serverUrl, channelId);
|
||||
await switchToChannel(serverUrl, channelId, teamId, skipLastUnread);
|
||||
openChannelIfNeeded(serverUrl, channelId);
|
||||
markChannelAsRead(serverUrl, channelId);
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
// 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 websocketManager from '@managers/websocket_manager';
|
||||
import {getBookmarksSince, getChannelBookmarkById} from '@queries/servers/channel_bookmark';
|
||||
import {getConfigValue} from '@queries/servers/system';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
export async function fetchChannelBookmarks(serverUrl: string, channelId: string, fetchOnly = false) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const bookmarksEnabled = (await getConfigValue(database, 'FeatureFlagChannelBookmarks')) === 'true';
|
||||
if (!bookmarksEnabled) {
|
||||
return {bookmarks: []};
|
||||
}
|
||||
|
||||
const since = await getBookmarksSince(database, channelId);
|
||||
const bookmarks = await client.getChannelBookmarksForChannel(channelId, since);
|
||||
|
||||
if (!fetchOnly && bookmarks.length) {
|
||||
await operator.handleChannelBookmark({bookmarks, prepareRecordsOnly: false});
|
||||
}
|
||||
|
||||
return {bookmarks};
|
||||
} catch (error) {
|
||||
logError('error on fetchChannelBookmarks', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function createChannelBookmark(serverUrl: string, channelId: string, bookmark: ChannelBookmark, fetchOnly = false) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const ws = websocketManager.getClient(serverUrl);
|
||||
|
||||
const created = await client.createChannelBookmark(channelId, bookmark, ws?.getConnectionId());
|
||||
if (!fetchOnly) {
|
||||
await operator.handleChannelBookmark({bookmarks: [created], prepareRecordsOnly: false});
|
||||
}
|
||||
return {bookmark: created};
|
||||
} catch (error) {
|
||||
logError('error on createChannelBookmark', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function editChannelBookmark(serverUrl: string, bookmark: ChannelBookmark, fetchOnly = false) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const ws = websocketManager.getClient(serverUrl);
|
||||
|
||||
const result = await client.updateChannelBookmark(bookmark.channel_id, bookmark, ws?.getConnectionId());
|
||||
const bookmarks = [result.updated];
|
||||
if (result.deleted) {
|
||||
bookmarks.push(result.deleted);
|
||||
}
|
||||
if (!fetchOnly) {
|
||||
await operator.handleChannelBookmark({bookmarks, prepareRecordsOnly: false});
|
||||
}
|
||||
return {bookmarks: result};
|
||||
} catch (error) {
|
||||
logError('error on editChannelBookmark', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteChannelBookmark(serverUrl: string, channelId: string, bookmarkId: string, fetchOnly = false) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const ws = websocketManager.getClient(serverUrl);
|
||||
|
||||
const result = await client.deleteChannelBookmark(channelId, bookmarkId, ws?.getConnectionId());
|
||||
|
||||
const bookmark = await getChannelBookmarkById(database, bookmarkId);
|
||||
if (bookmark && !fetchOnly) {
|
||||
const b = bookmark.toApi();
|
||||
b.delete_at = Date.now();
|
||||
await operator.handleChannelBookmark({bookmarks: [b], prepareRecordsOnly: false});
|
||||
}
|
||||
|
||||
return {bookmarks: result};
|
||||
} catch (error) {
|
||||
logError('error on deleteChannelBookmark', getFullErrorMessage(error));
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
|
@ -23,17 +23,16 @@ export const downloadProfileImage = (serverUrl: string, userId: string, lastPict
|
|||
|
||||
export const uploadFile = (
|
||||
serverUrl: string,
|
||||
file: FileInfo | ExtractedFileInfo,
|
||||
file: FileInfo,
|
||||
channelId: string,
|
||||
onProgress: (fractionCompleted: number, bytesRead?: number | null | undefined) => void = () => {/*Do Nothing*/},
|
||||
onComplete: (response: ClientResponse) => void = () => {/*Do Nothing*/},
|
||||
onError: (response: ClientResponseError) => void = () => {/*Do Nothing*/},
|
||||
skipBytes = 0,
|
||||
isBookmark = false,
|
||||
) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
return {cancel: client.uploadAttachment(file, channelId, onProgress, onComplete, onError, skipBytes, isBookmark)};
|
||||
return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)};
|
||||
} catch (error) {
|
||||
logDebug('error on uploadFile', getFullErrorMessage(error));
|
||||
return {error};
|
||||
|
|
|
|||
|
|
@ -153,9 +153,7 @@ export const searchFiles = async (serverUrl: string, teamId: string, params: Fil
|
|||
return acc;
|
||||
}, {});
|
||||
files.forEach((f) => {
|
||||
if (f.post_id) {
|
||||
f.postProps = idToPost[f.post_id]?.props;
|
||||
}
|
||||
f.postProps = idToPost[f.post_id]?.props;
|
||||
});
|
||||
return {files, channels};
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,289 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as bookmark from '@actions/local/channel_bookmark';
|
||||
import * as calls from '@calls/connection/websocket_event_handlers';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
import * as category from './category';
|
||||
import * as channel from './channel';
|
||||
import * as group from './group';
|
||||
import {handleOpenDialogEvent} from './integrations';
|
||||
import * as posts from './posts';
|
||||
import * as preferences from './preferences';
|
||||
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
|
||||
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
|
||||
import {handleLicenseChangedEvent, handleConfigChangedEvent} from './system';
|
||||
import * as teams from './teams';
|
||||
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
|
||||
import {handleUserUpdatedEvent, handleUserTypingEvent, handleStatusChangedEvent} from './users';
|
||||
|
||||
export async function handleWebSocketEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
case WebsocketEvents.EPHEMERAL_MESSAGE:
|
||||
posts.handleNewPostEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.POST_EDITED:
|
||||
posts.handlePostEdited(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.POST_DELETED:
|
||||
posts.handlePostDeleted(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.POST_UNREAD:
|
||||
posts.handlePostUnread(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.POST_ACKNOWLEDGEMENT_ADDED:
|
||||
posts.handlePostAcknowledgementAdded(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.POST_ACKNOWLEDGEMENT_REMOVED:
|
||||
posts.handlePostAcknowledgementRemoved(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.LEAVE_TEAM:
|
||||
teams.handleLeaveTeamEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.UPDATE_TEAM:
|
||||
teams.handleUpdateTeamEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.ADDED_TO_TEAM:
|
||||
teams.handleUserAddedToTeamEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.USER_ADDED:
|
||||
channel.handleUserAddedToChannelEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.USER_REMOVED:
|
||||
channel.handleUserRemovedFromChannelEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.USER_UPDATED:
|
||||
handleUserUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.ROLE_UPDATED:
|
||||
handleRoleUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.USER_ROLE_UPDATED:
|
||||
handleUserRoleUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.MEMBERROLE_UPDATED:
|
||||
handleTeamMemberRoleUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CATEGORY_CREATED:
|
||||
category.handleCategoryCreatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CATEGORY_UPDATED:
|
||||
category.handleCategoryUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CATEGORY_ORDER_UPDATED:
|
||||
category.handleCategoryOrderUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CATEGORY_DELETED:
|
||||
category.handleCategoryDeletedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_CREATED:
|
||||
channel.handleChannelCreatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_DELETED:
|
||||
channel.handleChannelDeletedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CHANNEL_UNARCHIVED:
|
||||
channel.handleChannelUnarchiveEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_UPDATED:
|
||||
channel.handleChannelUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_CONVERTED:
|
||||
channel.handleChannelConvertedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_VIEWED:
|
||||
channel.handleChannelViewedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.MULTIPLE_CHANNELS_VIEWED:
|
||||
channel.handleMultipleChannelsViewedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_MEMBER_UPDATED:
|
||||
channel.handleChannelMemberUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_SCHEME_UPDATED:
|
||||
// Do nothing, handled by CHANNEL_UPDATED due to changes in the channel scheme.
|
||||
break;
|
||||
|
||||
case WebsocketEvents.DIRECT_ADDED:
|
||||
case WebsocketEvents.GROUP_ADDED:
|
||||
channel.handleDirectAddedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.PREFERENCE_CHANGED:
|
||||
preferences.handlePreferenceChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.PREFERENCES_CHANGED:
|
||||
preferences.handlePreferencesChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.PREFERENCES_DELETED:
|
||||
preferences.handlePreferencesDeletedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.STATUS_CHANGED:
|
||||
handleStatusChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.TYPING:
|
||||
handleUserTypingEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.REACTION_ADDED:
|
||||
handleReactionAddedToPostEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.REACTION_REMOVED:
|
||||
handleReactionRemovedFromPostEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.EMOJI_ADDED:
|
||||
handleAddCustomEmoji(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.LICENSE_CHANGED:
|
||||
handleLicenseChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CONFIG_CHANGED:
|
||||
handleConfigChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.OPEN_DIALOG:
|
||||
handleOpenDialogEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.DELETE_TEAM:
|
||||
teams.handleTeamArchived(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.RESTORE_TEAM:
|
||||
teams.handleTeamRestored(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.THREAD_UPDATED:
|
||||
handleThreadUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.THREAD_READ_CHANGED:
|
||||
handleThreadReadChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.THREAD_FOLLOW_CHANGED:
|
||||
handleThreadFollowChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
// Calls ws events:
|
||||
case WebsocketEvents.CALLS_CHANNEL_ENABLED:
|
||||
calls.handleCallChannelEnabled(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
|
||||
calls.handleCallChannelDisabled(serverUrl, msg);
|
||||
break;
|
||||
|
||||
// DEPRECATED in favour of user_joined (since v0.21.0)
|
||||
case WebsocketEvents.CALLS_USER_CONNECTED:
|
||||
calls.handleCallUserConnected(serverUrl, msg);
|
||||
break;
|
||||
|
||||
// DEPRECATED in favour of user_left (since v0.21.0)
|
||||
case WebsocketEvents.CALLS_USER_DISCONNECTED:
|
||||
calls.handleCallUserDisconnected(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CALLS_USER_JOINED:
|
||||
calls.handleCallUserJoined(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_LEFT:
|
||||
calls.handleCallUserLeft(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_MUTED:
|
||||
calls.handleCallUserMuted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_UNMUTED:
|
||||
calls.handleCallUserUnmuted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_VOICE_ON:
|
||||
calls.handleCallUserVoiceOn(msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_VOICE_OFF:
|
||||
calls.handleCallUserVoiceOff(msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_CALL_START:
|
||||
calls.handleCallStarted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_SCREEN_ON:
|
||||
calls.handleCallScreenOn(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_SCREEN_OFF:
|
||||
calls.handleCallScreenOff(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_RAISE_HAND:
|
||||
calls.handleCallUserRaiseHand(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_UNRAISE_HAND:
|
||||
calls.handleCallUserUnraiseHand(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_CALL_END:
|
||||
calls.handleCallEnded(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_REACTED:
|
||||
calls.handleCallUserReacted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_RECORDING_STATE:
|
||||
calls.handleCallRecordingState(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_HOST_CHANGED:
|
||||
calls.handleCallHostChanged(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_DISMISSED_NOTIFICATION:
|
||||
calls.handleUserDismissedNotification(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.GROUP_RECEIVED:
|
||||
group.handleGroupReceivedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.GROUP_MEMBER_ADD:
|
||||
group.handleGroupMemberAddEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.GROUP_MEMBER_DELETE:
|
||||
group.handleGroupMemberDeleteEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.GROUP_ASSOCIATED_TO_TEAM:
|
||||
group.handleGroupTeamAssociatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.GROUP_DISSOCIATED_TO_TEAM:
|
||||
group.handleGroupTeamDissociateEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
// bookmarks
|
||||
case WebsocketEvents.CHANNEL_BOOKMARK_CREATED:
|
||||
case WebsocketEvents.CHANNEL_BOOKMARK_DELETED:
|
||||
bookmark.handleBookmarkAddedOrDeleted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CHANNEL_BOOKMARK_UPDATED:
|
||||
bookmark.handleBookmarkEdited(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CHANNEL_BOOKMARK_SORTED:
|
||||
bookmark.handleBookmarkSorted(serverUrl, msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {setAppInactiveSince} from '@actions/app/global';
|
||||
import {markChannelAsViewed} from '@actions/local/channel';
|
||||
import {dataRetentionCleanup} from '@actions/local/systems';
|
||||
import {markChannelAsRead} from '@actions/remote/channel';
|
||||
|
|
@ -14,10 +13,29 @@ import {
|
|||
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
|
||||
import {openAllUnreadChannels} from '@actions/remote/preference';
|
||||
import {autoUpdateTimezone} from '@actions/remote/user';
|
||||
import {getAppInactiveSince} from '@app/queries/app/global';
|
||||
import {loadConfigAndCalls} from '@calls/actions/calls';
|
||||
import {
|
||||
handleCallChannelDisabled,
|
||||
handleCallChannelEnabled,
|
||||
handleCallEnded,
|
||||
handleCallHostChanged,
|
||||
handleCallRecordingState,
|
||||
handleCallScreenOff,
|
||||
handleCallScreenOn,
|
||||
handleCallStarted, handleCallUserConnected, handleCallUserDisconnected,
|
||||
handleCallUserJoined,
|
||||
handleCallUserLeft,
|
||||
handleCallUserMuted,
|
||||
handleCallUserRaiseHand,
|
||||
handleCallUserReacted,
|
||||
handleCallUserUnmuted,
|
||||
handleCallUserUnraiseHand,
|
||||
handleCallUserVoiceOff,
|
||||
handleCallUserVoiceOn,
|
||||
handleUserDismissedNotification,
|
||||
} from '@calls/connection/websocket_event_handlers';
|
||||
import {isSupportedServerCalls} from '@calls/utils';
|
||||
import {Screens} from '@constants';
|
||||
import {Screens, WebsocketEvents} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
|
|
@ -38,6 +56,58 @@ import {setTeamLoading} from '@store/team_load_store';
|
|||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug, logInfo} from '@utils/log';
|
||||
|
||||
import {
|
||||
handleCategoryCreatedEvent,
|
||||
handleCategoryDeletedEvent,
|
||||
handleCategoryOrderUpdatedEvent,
|
||||
handleCategoryUpdatedEvent,
|
||||
} from './category';
|
||||
import {
|
||||
handleChannelConvertedEvent, handleChannelCreatedEvent,
|
||||
handleChannelDeletedEvent,
|
||||
handleChannelMemberUpdatedEvent,
|
||||
handleChannelUnarchiveEvent,
|
||||
handleChannelUpdatedEvent,
|
||||
handleChannelViewedEvent,
|
||||
handleMultipleChannelsViewedEvent,
|
||||
handleDirectAddedEvent,
|
||||
handleUserAddedToChannelEvent,
|
||||
handleUserRemovedFromChannelEvent,
|
||||
} from './channel';
|
||||
import {
|
||||
handleGroupMemberAddEvent,
|
||||
handleGroupMemberDeleteEvent,
|
||||
handleGroupReceivedEvent,
|
||||
handleGroupTeamAssociatedEvent,
|
||||
handleGroupTeamDissociateEvent,
|
||||
} from './group';
|
||||
import {handleOpenDialogEvent} from './integrations';
|
||||
import {
|
||||
handleNewPostEvent,
|
||||
handlePostAcknowledgementAdded,
|
||||
handlePostAcknowledgementRemoved,
|
||||
handlePostDeleted,
|
||||
handlePostEdited,
|
||||
handlePostUnread,
|
||||
} from './posts';
|
||||
import {
|
||||
handlePreferenceChangedEvent,
|
||||
handlePreferencesChangedEvent,
|
||||
handlePreferencesDeletedEvent,
|
||||
} from './preferences';
|
||||
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
|
||||
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
|
||||
import {handleLicenseChangedEvent, handleConfigChangedEvent} from './system';
|
||||
import {
|
||||
handleLeaveTeamEvent,
|
||||
handleUserAddedToTeamEvent,
|
||||
handleUpdateTeamEvent,
|
||||
handleTeamArchived,
|
||||
handleTeamRestored,
|
||||
} from './teams';
|
||||
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
|
||||
import {handleUserUpdatedEvent, handleUserTypingEvent, handleStatusChangedEvent} from './users';
|
||||
|
||||
export async function handleFirstConnect(serverUrl: string) {
|
||||
registerDeviceToken(serverUrl);
|
||||
autoUpdateTimezone(serverUrl);
|
||||
|
|
@ -77,11 +147,7 @@ async function doReconnect(serverUrl: string) {
|
|||
|
||||
const {database} = operator;
|
||||
|
||||
let lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
if (!lastDisconnectedAt) {
|
||||
lastDisconnectedAt = await getAppInactiveSince();
|
||||
}
|
||||
setAppInactiveSince(0);
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
resetWebSocketLastDisconnected(operator);
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
|
|
@ -131,6 +197,280 @@ async function doReconnect(serverUrl: string) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
case WebsocketEvents.EPHEMERAL_MESSAGE:
|
||||
handleNewPostEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.POST_EDITED:
|
||||
handlePostEdited(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.POST_DELETED:
|
||||
handlePostDeleted(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.POST_UNREAD:
|
||||
handlePostUnread(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.POST_ACKNOWLEDGEMENT_ADDED:
|
||||
handlePostAcknowledgementAdded(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.POST_ACKNOWLEDGEMENT_REMOVED:
|
||||
handlePostAcknowledgementRemoved(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.LEAVE_TEAM:
|
||||
handleLeaveTeamEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.UPDATE_TEAM:
|
||||
handleUpdateTeamEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.ADDED_TO_TEAM:
|
||||
handleUserAddedToTeamEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.USER_ADDED:
|
||||
handleUserAddedToChannelEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.USER_REMOVED:
|
||||
handleUserRemovedFromChannelEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.USER_UPDATED:
|
||||
handleUserUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.ROLE_UPDATED:
|
||||
handleRoleUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.USER_ROLE_UPDATED:
|
||||
handleUserRoleUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.MEMBERROLE_UPDATED:
|
||||
handleTeamMemberRoleUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CATEGORY_CREATED:
|
||||
handleCategoryCreatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CATEGORY_UPDATED:
|
||||
handleCategoryUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CATEGORY_ORDER_UPDATED:
|
||||
handleCategoryOrderUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CATEGORY_DELETED:
|
||||
handleCategoryDeletedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_CREATED:
|
||||
handleChannelCreatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_DELETED:
|
||||
handleChannelDeletedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CHANNEL_UNARCHIVED:
|
||||
handleChannelUnarchiveEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_UPDATED:
|
||||
handleChannelUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_CONVERTED:
|
||||
handleChannelConvertedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_VIEWED:
|
||||
handleChannelViewedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.MULTIPLE_CHANNELS_VIEWED:
|
||||
handleMultipleChannelsViewedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_MEMBER_UPDATED:
|
||||
handleChannelMemberUpdatedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CHANNEL_SCHEME_UPDATED:
|
||||
// Do nothing, handled by CHANNEL_UPDATED due to changes in the channel scheme.
|
||||
break;
|
||||
|
||||
case WebsocketEvents.DIRECT_ADDED:
|
||||
case WebsocketEvents.GROUP_ADDED:
|
||||
handleDirectAddedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.PREFERENCE_CHANGED:
|
||||
handlePreferenceChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.PREFERENCES_CHANGED:
|
||||
handlePreferencesChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.PREFERENCES_DELETED:
|
||||
handlePreferencesDeletedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.STATUS_CHANGED:
|
||||
handleStatusChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.TYPING:
|
||||
handleUserTypingEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.REACTION_ADDED:
|
||||
handleReactionAddedToPostEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.REACTION_REMOVED:
|
||||
handleReactionRemovedFromPostEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.EMOJI_ADDED:
|
||||
handleAddCustomEmoji(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.LICENSE_CHANGED:
|
||||
handleLicenseChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CONFIG_CHANGED:
|
||||
handleConfigChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.OPEN_DIALOG:
|
||||
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;
|
||||
|
||||
case WebsocketEvents.THREAD_READ_CHANGED:
|
||||
handleThreadReadChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.THREAD_FOLLOW_CHANGED:
|
||||
handleThreadFollowChangedEvent(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS:
|
||||
break;
|
||||
|
||||
// return dispatch(handleRefreshAppsBindings());
|
||||
|
||||
// Calls ws events:
|
||||
case WebsocketEvents.CALLS_CHANNEL_ENABLED:
|
||||
handleCallChannelEnabled(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
|
||||
handleCallChannelDisabled(serverUrl, msg);
|
||||
break;
|
||||
|
||||
// DEPRECATED in favour of user_joined (since v0.21.0)
|
||||
case WebsocketEvents.CALLS_USER_CONNECTED:
|
||||
handleCallUserConnected(serverUrl, msg);
|
||||
break;
|
||||
|
||||
// DEPRECATED in favour of user_left (since v0.21.0)
|
||||
case WebsocketEvents.CALLS_USER_DISCONNECTED:
|
||||
handleCallUserDisconnected(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.CALLS_USER_JOINED:
|
||||
handleCallUserJoined(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_LEFT:
|
||||
handleCallUserLeft(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_MUTED:
|
||||
handleCallUserMuted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_UNMUTED:
|
||||
handleCallUserUnmuted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_VOICE_ON:
|
||||
handleCallUserVoiceOn(msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_VOICE_OFF:
|
||||
handleCallUserVoiceOff(msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_CALL_START:
|
||||
handleCallStarted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_SCREEN_ON:
|
||||
handleCallScreenOn(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_SCREEN_OFF:
|
||||
handleCallScreenOff(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_RAISE_HAND:
|
||||
handleCallUserRaiseHand(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_UNRAISE_HAND:
|
||||
handleCallUserUnraiseHand(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_CALL_END:
|
||||
handleCallEnded(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_REACTED:
|
||||
handleCallUserReacted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_RECORDING_STATE:
|
||||
handleCallRecordingState(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_HOST_CHANGED:
|
||||
handleCallHostChanged(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_DISMISSED_NOTIFICATION:
|
||||
handleUserDismissedNotification(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.GROUP_RECEIVED:
|
||||
handleGroupReceivedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.GROUP_MEMBER_ADD:
|
||||
handleGroupMemberAddEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.GROUP_MEMBER_DELETE:
|
||||
handleGroupMemberDeleteEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.GROUP_ASSOCIATED_TO_TEAM:
|
||||
handleGroupTeamAssociatedEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.GROUP_DISSOCIATED_TO_TEAM:
|
||||
handleGroupTeamDissociateEvent(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.GROUP_ASSOCIATED_TO_CHANNEL:
|
||||
break;
|
||||
case WebsocketEvents.GROUP_DISSOCIATED_TO_CHANNEL:
|
||||
break;
|
||||
|
||||
// Plugins
|
||||
case WebsocketEvents.PLUGIN_STATUSES_CHANGED:
|
||||
case WebsocketEvents.PLUGIN_ENABLED:
|
||||
case WebsocketEvents.PLUGIN_DISABLED:
|
||||
// Do nothing, this event doesn't need logic in the mobile app
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPostDataIfNeeded(serverUrl: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
|
|||
|
|
@ -134,14 +134,6 @@ export default class ClientBase {
|
|||
return `${this.getChannelsRoute()}/${channelId}`;
|
||||
}
|
||||
|
||||
getChannelBookmarksRoute(channelId: string) {
|
||||
return `${this.getChannelRoute(channelId)}/bookmarks`;
|
||||
}
|
||||
|
||||
getChannelBookmarkRoute(channelId: string, bookmarkId: string) {
|
||||
return `${this.getChannelBookmarksRoute(channelId)}/${bookmarkId}`;
|
||||
}
|
||||
|
||||
getSharedChannelsRoute() {
|
||||
return `${this.urlVersion}/sharedchannels`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@utils/helpers';
|
||||
|
||||
import type ClientBase from './base';
|
||||
|
||||
export interface ClientChannelBookmarksMix {
|
||||
createChannelBookmark(channelId: string, bookmark: ChannelBookmark, connectionId?: string): Promise<ChannelBookmarkWithFileInfo>;
|
||||
updateChannelBookmark(channelId: string, bookmark: ChannelBookmark, connectionId?: string): Promise<UpdateChannelBookmarkResponse>;
|
||||
updateChannelBookmarkSortOrder(channelId: string, bookmarkId: string, newSortOrder: number, connectionId?: string): Promise<ChannelBookmarkWithFileInfo[]>;
|
||||
deleteChannelBookmark(channelId: string, bookmarkId: string, connectionId?: string): Promise<ChannelBookmarkWithFileInfo>;
|
||||
getChannelBookmarksForChannel(channelId: string, since: number): Promise<ChannelBookmarkWithFileInfo[]>;
|
||||
}
|
||||
|
||||
const ClientChannelBookmarks = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
createChannelBookmark = async (channelId: string, bookmark: ChannelBookmark, connectionId = '') => {
|
||||
this.analytics?.trackAPI('api_channels_bookmark_create', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
this.getChannelBookmarksRoute(channelId),
|
||||
{
|
||||
method: 'post',
|
||||
body: bookmark,
|
||||
headers: {'Connection-Id': connectionId},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannelBookmark(channelId: string, bookmark: ChannelBookmark, connectionId?: string) {
|
||||
this.analytics?.trackAPI('api_channels_bookmark_update', {channel_id: channelId, bookmark_id: bookmark.id});
|
||||
|
||||
return this.doFetch(
|
||||
this.getChannelBookmarkRoute(channelId, bookmark.id),
|
||||
{
|
||||
method: 'patch',
|
||||
body: bookmark,
|
||||
headers: {'Connection-Id': connectionId},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
updateChannelBookmarkSortOrder(channelId: string, bookmarkId: string, newSortOrder: number, connectionId?: string) {
|
||||
this.analytics?.trackAPI('api_channels_bookmark_sort_order', {channel_id: channelId, bookmark_id: bookmarkId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelBookmarkRoute(channelId, bookmarkId)}/sort_order`,
|
||||
{
|
||||
method: 'post',
|
||||
body: newSortOrder,
|
||||
headers: {'Connection-Id': connectionId},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
deleteChannelBookmark(channelId: string, bookmarkId: string, connectionId?: string) {
|
||||
this.analytics?.trackAPI('api_channels_bookmark_delete', {channel_id: channelId, bookmark_id: bookmarkId});
|
||||
|
||||
return this.doFetch(
|
||||
this.getChannelBookmarkRoute(channelId, bookmarkId),
|
||||
{
|
||||
method: 'delete',
|
||||
headers: {'Connection-Id': connectionId},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
getChannelBookmarksForChannel(channelId: string, since: number) {
|
||||
this.analytics?.trackAPI('api_channels_bookmark_get', {channel_id: channelId});
|
||||
return this.doFetch(
|
||||
`${this.getChannelBookmarksRoute(channelId)}${buildQueryString({bookmarks_since: since})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientChannelBookmarks;
|
||||
|
|
@ -152,16 +152,14 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
this.analytics?.trackAPI('api_channel_get', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
this.getChannelRoute(channelId),
|
||||
`${this.getChannelRoute(channelId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelByName = async (teamId: string, channelName: string, includeDeleted = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/name/${channelName}${buildQueryString({
|
||||
include_deleted: includeDeleted,
|
||||
})}`,
|
||||
`${this.getTeamRoute(teamId)}/channels/name/${channelName}?include_deleted=${includeDeleted}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
|
@ -177,20 +175,14 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
|
||||
getChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels${buildQueryString({
|
||||
page,
|
||||
per_page: perPage,
|
||||
})}`,
|
||||
`${this.getTeamRoute(teamId)}/channels${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getArchivedChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/deleted${buildQueryString({
|
||||
page,
|
||||
per_page: perPage,
|
||||
})}`,
|
||||
`${this.getTeamRoute(teamId)}/channels/deleted${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
|
@ -202,11 +194,11 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
);
|
||||
};
|
||||
|
||||
getMyChannels = async (teamId: string, includeDeleted = false, since = 0) => {
|
||||
getMyChannels = async (teamId: string, includeDeleted = false, lastDeleteAt = 0) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/${teamId}/channels${buildQueryString({
|
||||
include_deleted: includeDeleted,
|
||||
last_delete_at: since,
|
||||
last_delete_at: lastDeleteAt,
|
||||
})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,14 +11,13 @@ export interface ClientFilesMix {
|
|||
getFileThumbnailUrl: (fileId: string, timestamp: number) => string;
|
||||
getFilePreviewUrl: (fileId: string, timestamp: number) => string;
|
||||
getFilePublicLink: (fileId: string) => Promise<{link: string}>;
|
||||
uploadAttachment: (
|
||||
file: FileInfo | ExtractedFileInfo,
|
||||
uploadPostAttachment: (
|
||||
file: FileInfo,
|
||||
channelId: string,
|
||||
onProgress: (fractionCompleted: number, bytesRead?: number | null | undefined) => void,
|
||||
onComplete: (response: ClientResponse) => void,
|
||||
onError: (response: ClientResponseError) => void,
|
||||
skipBytes?: number,
|
||||
isBookmark?: boolean,
|
||||
) => () => void;
|
||||
searchFiles: (teamId: string, terms: string) => Promise<FileSearchRequest>;
|
||||
searchFilesWithParams: (teamId: string, FileSearchParams: string) => Promise<FileSearchRequest>;
|
||||
|
|
@ -59,19 +58,15 @@ const ClientFiles = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
uploadAttachment = (
|
||||
file: FileInfo | ExtractedFileInfo,
|
||||
uploadPostAttachment = (
|
||||
file: FileInfo,
|
||||
channelId: string,
|
||||
onProgress: (fractionCompleted: number, bytesRead?: number | null | undefined) => void,
|
||||
onComplete: (response: ClientResponse) => void,
|
||||
onError: (response: ClientResponseError) => void,
|
||||
skipBytes = 0,
|
||||
isBookmark = false,
|
||||
) => {
|
||||
let url = this.getFilesRoute();
|
||||
if (isBookmark) {
|
||||
url = `${url}?bookmark=true`;
|
||||
}
|
||||
const url = this.getFilesRoute();
|
||||
const options: UploadRequestOptions = {
|
||||
skipBytes,
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import mix from '@utils/mix';
|
|||
import ClientApps, {type ClientAppsMix} from './apps';
|
||||
import ClientBase from './base';
|
||||
import ClientCategories, {type ClientCategoriesMix} from './categories';
|
||||
import ClientChannelBookmarks, {type ClientChannelBookmarksMix} from './channel_bookmark';
|
||||
import ClientChannels, {type ClientChannelsMix} from './channels';
|
||||
import {DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID} from './constants';
|
||||
import ClientEmojis, {type ClientEmojisMix} from './emojis';
|
||||
|
|
@ -30,7 +29,6 @@ interface Client extends ClientBase,
|
|||
ClientAppsMix,
|
||||
ClientCategoriesMix,
|
||||
ClientChannelsMix,
|
||||
ClientChannelBookmarksMix,
|
||||
ClientEmojisMix,
|
||||
ClientFilesMix,
|
||||
ClientGeneralMix,
|
||||
|
|
@ -51,7 +49,6 @@ class Client extends mix(ClientBase).with(
|
|||
ClientApps,
|
||||
ClientCategories,
|
||||
ClientChannels,
|
||||
ClientChannelBookmarks,
|
||||
ClientEmojis,
|
||||
ClientFiles,
|
||||
ClientGeneral,
|
||||
|
|
|
|||
|
|
@ -383,8 +383,4 @@ export default class WebSocketClient {
|
|||
public isConnected(): boolean {
|
||||
return this.conn?.readyState === WebSocketReadyState.OPEN; //|| (!this.stop && this.connectFailCount <= 2);
|
||||
}
|
||||
|
||||
public getConnectionId(): string {
|
||||
return this.connectionId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo, type ReactNode} from 'react';
|
||||
import {type StyleProp, StyleSheet, Text, type TextStyle, View, type ViewStyle, type Insets} from 'react-native';
|
||||
import {type StyleProp, StyleSheet, Text, type TextStyle, View, type ViewStyle} from 'react-native';
|
||||
import RNButton from 'react-native-button';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type ConditionalProps = | {iconName: string; iconSize: number} | {iconName?: never; iconSize?: never}
|
||||
|
||||
|
|
@ -23,8 +22,6 @@ type Props = ConditionalProps & {
|
|||
onPress: () => void;
|
||||
text: string;
|
||||
iconComponent?: ReactNode;
|
||||
disabled?: boolean;
|
||||
hitSlop?: Insets;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
|
@ -46,8 +43,6 @@ const Button = ({
|
|||
iconName,
|
||||
iconSize,
|
||||
iconComponent,
|
||||
disabled,
|
||||
hitSlop,
|
||||
}: Props) => {
|
||||
const bgStyle = useMemo(() => [
|
||||
buttonBackgroundStyle(theme, size, emphasis, buttonType, buttonState),
|
||||
|
|
@ -68,14 +63,6 @@ const Button = ({
|
|||
[iconSize],
|
||||
);
|
||||
|
||||
let buttonContainerStyle = StyleSheet.flatten(bgStyle);
|
||||
if (disabled) {
|
||||
buttonContainerStyle = {
|
||||
...buttonContainerStyle,
|
||||
backgroundColor: changeOpacity(buttonContainerStyle.backgroundColor! as string, 0.4),
|
||||
};
|
||||
}
|
||||
|
||||
let icon: ReactNode;
|
||||
|
||||
if (iconComponent) {
|
||||
|
|
@ -93,11 +80,9 @@ const Button = ({
|
|||
|
||||
return (
|
||||
<RNButton
|
||||
containerStyle={buttonContainerStyle}
|
||||
containerStyle={bgStyle}
|
||||
onPress={onPress}
|
||||
testID={testID}
|
||||
disabled={disabled}
|
||||
hitSlop={hitSlop}
|
||||
>
|
||||
<View style={containerStyle}>
|
||||
{icon}
|
||||
|
|
|
|||
|
|
@ -1,163 +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 {Alert, View, type Insets} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import Button from '@components/button';
|
||||
import {ITEM_HEIGHT} from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {TITLE_HEIGHT} from '@screens/bottom_sheet';
|
||||
import {bottomSheet, showModal} from '@screens/navigation';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import CompassIcon from '../compass_icon';
|
||||
|
||||
import AddBookmarkOptions from './add_bookmark_options';
|
||||
|
||||
type Props = {
|
||||
bookmarksCount: number;
|
||||
canUploadFiles: boolean;
|
||||
channelId: string;
|
||||
currentUserId: string;
|
||||
showLarge: boolean;
|
||||
}
|
||||
|
||||
const MAX_BOOKMARKS_PER_CHANNEL = 50;
|
||||
const hitSlop: Insets = {top: 10, bottom: 10, left: 10, right: 10};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
container: {
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: 16,
|
||||
},
|
||||
largeButton: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
height: 32,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 0,
|
||||
borderRadius: 16,
|
||||
},
|
||||
largeButtonText: {
|
||||
color: theme.centerChannelColor,
|
||||
lineHeight: undefined,
|
||||
marginTop: undefined,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
paddingRight: 6,
|
||||
},
|
||||
largeButtonIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
paddingRight: 4,
|
||||
paddingTop: 3,
|
||||
},
|
||||
smallButton: {
|
||||
backgroundColor: undefined,
|
||||
paddingHorizontal: undefined,
|
||||
paddingVertical: undefined,
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
margin: undefined,
|
||||
top: 3,
|
||||
right: 0,
|
||||
},
|
||||
smallButtonText: {
|
||||
color: theme.centerChannelColor,
|
||||
lineHeight: undefined,
|
||||
marginTop: undefined,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
marginRight: undefined,
|
||||
padding: undefined,
|
||||
},
|
||||
smallButtonIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
},
|
||||
}));
|
||||
|
||||
const AddBookmark = ({bookmarksCount, channelId, currentUserId, canUploadFiles, showLarge}: Props) => {
|
||||
const theme = useTheme();
|
||||
const {formatMessage} = useIntl();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
if (bookmarksCount >= MAX_BOOKMARKS_PER_CHANNEL) {
|
||||
Alert.alert(
|
||||
formatMessage({id: 'channel_info.add_bookmark', defaultMessage: 'Add a bookmark'}),
|
||||
formatMessage({
|
||||
id: 'channel_info.add_bookmark.max_reached',
|
||||
defaultMessage: 'This channel has reached the maximum number of bookmarks ({count}).',
|
||||
}, {count: MAX_BOOKMARKS_PER_CHANNEL}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canUploadFiles) {
|
||||
const title = formatMessage({id: 'screens.channel_bookmark_add', defaultMessage: 'Add a bookmark'});
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-channel-bookmark-add';
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: 'close.channel_bookmark_add.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
showModal(Screens.CHANNEL_BOOKMARK, title, {
|
||||
channelId,
|
||||
closeButtonId,
|
||||
type: 'link',
|
||||
ownerId: currentUserId,
|
||||
}, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const renderContent = () => (
|
||||
<AddBookmarkOptions
|
||||
channelId={channelId}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
);
|
||||
|
||||
bottomSheet({
|
||||
title: formatMessage({id: 'channel_info.add_bookmark', defaultMessage: 'Add a bookmark'}),
|
||||
renderContent,
|
||||
snapPoints: [1, bottomSheetSnapPoint(1, (2 * ITEM_HEIGHT), bottom) + TITLE_HEIGHT],
|
||||
theme,
|
||||
closeButtonId: 'close-channel-quick-actions',
|
||||
});
|
||||
}, [bottom, bookmarksCount, canUploadFiles, currentUserId, channelId, theme]);
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
backgroundStyle={showLarge ? styles.largeButton : styles.smallButton}
|
||||
onPress={onPress}
|
||||
hitSlop={hitSlop}
|
||||
text={showLarge ? formatMessage({id: 'channel_info.add_bookmark', defaultMessage: 'Add a bookmark'}) : ''}
|
||||
textStyle={showLarge ? styles.largeButtonText : styles.smallButtonText}
|
||||
theme={theme}
|
||||
iconComponent={
|
||||
<CompassIcon
|
||||
name='plus'
|
||||
size={showLarge ? 16 : 20}
|
||||
style={showLarge ? styles.largeButtonIcon : styles.smallButtonIcon}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
if (showLarge) {
|
||||
return (<View style={styles.container}>{button}</View>);
|
||||
}
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
export default AddBookmark;
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import BookmarkType from '@components/channel_bookmarks/bookmark_type';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
currentUserId: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
flex: {flex: 1},
|
||||
listHeader: {marginBottom: 12},
|
||||
listHeaderText: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const AddBookmarkOptions = ({channelId, currentUserId}: Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isTablet && (
|
||||
<View style={styles.listHeader}>
|
||||
<FormattedText
|
||||
id='channel_info.add_bookmark'
|
||||
defaultMessage={'Add a bookmark'}
|
||||
style={styles.listHeaderText}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.flex}>
|
||||
<BookmarkType
|
||||
channelId={channelId}
|
||||
type='link'
|
||||
ownerId={currentUserId}
|
||||
/>
|
||||
<BookmarkType
|
||||
channelId={channelId}
|
||||
type='file'
|
||||
ownerId={currentUserId}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddBookmarkOptions;
|
||||
|
|
@ -1,61 +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 CompassIcon from '@components/compass_icon';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissBottomSheet, showModal} from '@screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
type: ChannelBookmarkType;
|
||||
ownerId: string;
|
||||
}
|
||||
|
||||
const BookmarkType = ({channelId, type, ownerId}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
|
||||
const onPress = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
const title = formatMessage({id: 'screens.channel_bookmark_add', defaultMessage: 'Add a bookmark'});
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-channel-bookmark-add';
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: 'close.channel_bookmark_add.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
showModal(Screens.CHANNEL_BOOKMARK, title, {channelId, closeButtonId, type, ownerId}, options);
|
||||
}, [channelId, theme, type, ownerId]);
|
||||
|
||||
let icon;
|
||||
let label;
|
||||
if (type === 'link') {
|
||||
icon = 'link-variant';
|
||||
label = formatMessage({id: 'channel_info.add_bookmark.link', defaultMessage: 'Add a link'});
|
||||
} else {
|
||||
icon = 'paperclip';
|
||||
label = formatMessage({id: 'channel_info.add_bookmark.file', defaultMessage: 'Attach a file'});
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
action={onPress}
|
||||
label={label}
|
||||
icon={icon}
|
||||
type='default'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkType;
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo, type ReactNode} from 'react';
|
||||
import {View, Text, Platform} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import BookmarkIcon from './bookmark_icon';
|
||||
|
||||
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
|
||||
type Props = {
|
||||
bookmark: ChannelBookmarkModel;
|
||||
children?: ReactNode;
|
||||
file?: FileModel;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
imageContainer: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
image: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
marginLeft: 2,
|
||||
},
|
||||
emoji: {
|
||||
alignSelf: 'center',
|
||||
top: Platform.select({android: -2}),
|
||||
},
|
||||
genericBookmark: {
|
||||
top: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
const BookmarkDetails = ({bookmark, children, file}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const fileInfo = useMemo(() => file?.toFileInfo(bookmark.ownerId), [file, bookmark.ownerId]);
|
||||
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<View style={styles.imageContainer}>
|
||||
<BookmarkIcon
|
||||
emoji={bookmark.emoji}
|
||||
emojiSize={Platform.select({ios: 20, default: 19})}
|
||||
emojiStyle={styles.emoji}
|
||||
file={fileInfo}
|
||||
genericStyle={styles.genericBookmark}
|
||||
iconSize={24}
|
||||
imageStyle={styles.image}
|
||||
imageUrl={bookmark.imageUrl}
|
||||
/>
|
||||
{children}
|
||||
</View>
|
||||
<Text style={styles.text}>{bookmark.displayName}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkDetails;
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
|
||||
import ProgressBar from '@app/components/progress_bar';
|
||||
import {useTheme} from '@app/context/theme';
|
||||
import Document, {type DocumentRef} from '@components/document';
|
||||
|
||||
import BookmarkDetails from './bookmark_details';
|
||||
|
||||
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
|
||||
type Props = {
|
||||
bookmark: ChannelBookmarkModel;
|
||||
canDownloadFiles: boolean;
|
||||
file: FileModel;
|
||||
onLongPress: () => void;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 6,
|
||||
},
|
||||
progress: {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
});
|
||||
|
||||
const BookmarkDocument = ({bookmark, canDownloadFiles, file, onLongPress}: Props) => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const document = useRef<DocumentRef>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
const handlePress = useCallback(async () => {
|
||||
if (document.current) {
|
||||
document.current.handlePreviewPress();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Document
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={file.toFileInfo(bookmark.ownerId)}
|
||||
onProgress={setProgress}
|
||||
ref={document}
|
||||
>
|
||||
<Button
|
||||
onPress={handlePress}
|
||||
onLongPress={onLongPress}
|
||||
containerStyle={styles.container}
|
||||
>
|
||||
<BookmarkDetails
|
||||
bookmark={bookmark}
|
||||
file={file}
|
||||
>
|
||||
<View style={[StyleSheet.absoluteFill, styles.progress]}>
|
||||
{progress > 0 &&
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</BookmarkDetails>
|
||||
</Button>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkDocument;
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useCallback} from 'react';
|
||||
import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native';
|
||||
import FastImage, {type ImageStyle} from 'react-native-fast-image';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Emoji from '@components/emoji';
|
||||
import FileIcon from '@components/files/file_icon';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
type Props = {
|
||||
emoji?: string;
|
||||
emojiSize: number;
|
||||
emojiStyle?: StyleProp<TextStyle>;
|
||||
file?: FileInfo | ExtractedFileInfo;
|
||||
iconSize: number;
|
||||
imageStyle?: StyleProp<ImageStyle>;
|
||||
imageUrl?: string;
|
||||
genericStyle: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const BookmarkIcon = ({emoji, emojiSize, emojiStyle, file, genericStyle, iconSize, imageStyle, imageUrl}: Props) => {
|
||||
const theme = useTheme();
|
||||
const [hasImageError, setHasImageError] = useState(false);
|
||||
|
||||
const handleImageError = useCallback(() => {
|
||||
setHasImageError(true);
|
||||
}, []);
|
||||
|
||||
if (file && !emoji && !hasImageError) {
|
||||
return (
|
||||
<FileIcon
|
||||
file={file}
|
||||
iconSize={iconSize}
|
||||
smallImage={true}
|
||||
/>
|
||||
);
|
||||
} else if (imageUrl && !emoji && !hasImageError) {
|
||||
return (
|
||||
<FastImage
|
||||
source={{uri: imageUrl}}
|
||||
style={imageStyle}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
);
|
||||
} else if (emoji) {
|
||||
return (
|
||||
<Emoji
|
||||
emojiName={emoji!}
|
||||
size={emojiSize}
|
||||
textStyle={emojiStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CompassIcon
|
||||
name='book-outline'
|
||||
size={22}
|
||||
color={theme.centerChannelColor}
|
||||
style={genericStyle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkIcon;
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert, Text, View} from 'react-native';
|
||||
import Share from 'react-native-share';
|
||||
|
||||
import {deleteChannelBookmark} from '@actions/remote/channel_bookmark';
|
||||
import {fetchPublicLink} from '@actions/remote/file';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import DownloadWithAction from '@screens/gallery/footer/download_with_action';
|
||||
import {dismissBottomSheet, showModal, showOverlay} from '@screens/navigation';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {isImage, isVideo} from '@utils/file';
|
||||
import {showSnackBar} from '@utils/snack_bar';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
import type {GalleryAction, GalleryFileType, GalleryItemType} from '@typings/screens/gallery';
|
||||
|
||||
type Props = {
|
||||
bookmark: ChannelBookmarkModel;
|
||||
canCopyPublicLink: boolean;
|
||||
canDeleteBookmarks: boolean;
|
||||
canDownloadFiles: boolean;
|
||||
canEditBookmarks: boolean;
|
||||
file?: FileModel;
|
||||
setAction: React.Dispatch<React.SetStateAction<GalleryAction>>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
flex: {flex: 1},
|
||||
header: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
headerText: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 600, 'SemiBold'),
|
||||
},
|
||||
}));
|
||||
|
||||
const ChannelBookmarkOptions = ({
|
||||
bookmark,
|
||||
canCopyPublicLink,
|
||||
canDeleteBookmarks,
|
||||
canDownloadFiles,
|
||||
canEditBookmarks,
|
||||
file,
|
||||
setAction,
|
||||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const intl = useIntl();
|
||||
const styles = getStyleSheet(theme);
|
||||
const canShare = canDownloadFiles || bookmark.type === 'link';
|
||||
|
||||
const isVideoFile = useMemo(() => isVideo(file), [file]);
|
||||
const isImageFile = useMemo(() => isImage(file), [file]);
|
||||
const galleryItem = useMemo(() => {
|
||||
if (file) {
|
||||
const fileInfo = file.toFileInfo(bookmark.ownerId);
|
||||
let type: GalleryFileType = 'file';
|
||||
if (isImageFile) {
|
||||
type = 'image';
|
||||
} else if (isVideoFile) {
|
||||
type = 'video';
|
||||
}
|
||||
const item: GalleryItemType = {
|
||||
...fileInfo,
|
||||
id: fileInfo.id!,
|
||||
type,
|
||||
lastPictureUpdate: 0,
|
||||
uri: '',
|
||||
};
|
||||
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}, [bookmark, file]);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
const {error} = await deleteChannelBookmark(serverUrl, bookmark.channelId, bookmark.id);
|
||||
if (error) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'channel_bookmark.delete.failed_title', defaultMessage: 'Error deleting bookmark'}),
|
||||
intl.formatMessage({id: 'channel_bookmark.delete.failed_detail', defaultMessage: 'Details: {error}'}, {
|
||||
error: getFullErrorMessage(error),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await dismissBottomSheet();
|
||||
}, [bookmark, intl, serverUrl]);
|
||||
|
||||
const onCopy = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
|
||||
if (bookmark.type === 'link' && bookmark.linkUrl) {
|
||||
Clipboard.setString(bookmark.linkUrl);
|
||||
showSnackBar({barType: 'LINK_COPIED'});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const publicLink = await fetchPublicLink(serverUrl, bookmark.fileId!);
|
||||
if ('link' in publicLink) {
|
||||
Clipboard.setString(publicLink.link);
|
||||
showSnackBar({barType: 'LINK_COPIED'});
|
||||
} else {
|
||||
showSnackBar({barType: 'LINK_COPY_FAILED'});
|
||||
}
|
||||
} catch {
|
||||
showSnackBar({barType: 'LINK_COPY_FAILED'});
|
||||
}
|
||||
}, [bookmark, serverUrl]);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'channel_bookmark.delete.confirm_title', defaultMessage: 'Delete bookmark'}),
|
||||
intl.formatMessage({id: 'channel_bookmark.delete.confirm', defaultMessage: 'You sure want to delete the bookmark {displayName}?'}, {
|
||||
displayName: bookmark.displayName,
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({id: 'channel_bookmark.delete.yes', defaultMessage: 'Yes'}),
|
||||
style: 'destructive',
|
||||
isPreferred: true,
|
||||
onPress: handleDelete,
|
||||
}, {
|
||||
text: intl.formatMessage({id: 'channel_bookmark.add.file_cancel', defaultMessage: 'Cancel'}),
|
||||
style: 'cancel',
|
||||
}],
|
||||
);
|
||||
}, [bookmark, handleDelete]);
|
||||
|
||||
const onEdit = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
|
||||
const title = intl.formatMessage({id: 'screens.channel_bookmark_edit', defaultMessage: 'Edit bookmark'});
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
|
||||
const closeButtonId = 'close-channel_bookmark_edit';
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: 'close.channel_bookmark_edit.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
showModal(Screens.CHANNEL_BOOKMARK, title, {
|
||||
bookmark: bookmark.toApi(),
|
||||
canDeleteBookmarks,
|
||||
channelId: bookmark.channelId,
|
||||
closeButtonId,
|
||||
file: file?.toFileInfo(bookmark.ownerId),
|
||||
ownerId: bookmark.ownerId,
|
||||
type: bookmark.type,
|
||||
}, options);
|
||||
}, [bookmark, canDeleteBookmarks, file, intl, theme]);
|
||||
|
||||
const onShare = useCallback(async () => {
|
||||
await dismissBottomSheet();
|
||||
|
||||
if (bookmark.type === 'file') {
|
||||
if (file) {
|
||||
setAction('sharing');
|
||||
showOverlay(Screens.GENERIC_OVERLAY, {
|
||||
children: (
|
||||
<DownloadWithAction
|
||||
action={'sharing'}
|
||||
galleryView={false}
|
||||
item={galleryItem!}
|
||||
setAction={setAction}
|
||||
/>
|
||||
),
|
||||
}, {}, bookmark.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bookmark.type === 'link') {
|
||||
const title = bookmark.displayName;
|
||||
const url = bookmark.linkUrl!;
|
||||
Share.open({
|
||||
title,
|
||||
message: title,
|
||||
url,
|
||||
showAppsToView: true,
|
||||
}).catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
}
|
||||
}, [bookmark, file, serverUrl, setAction]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isTablet && (
|
||||
<View style={styles.header}>
|
||||
<Text
|
||||
style={styles.headerText}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{bookmark.displayName}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.flex}>
|
||||
{canEditBookmarks &&
|
||||
<OptionItem
|
||||
action={onEdit}
|
||||
label={intl.formatMessage({id: 'channel_bookmark.edit_option', defaultMessage: 'Edit'})}
|
||||
icon='pencil-outline'
|
||||
type='default'
|
||||
/>
|
||||
}
|
||||
{canCopyPublicLink &&
|
||||
<OptionItem
|
||||
action={onCopy}
|
||||
label={intl.formatMessage({id: 'channel_bookmark.copy_option', defaultMessage: 'Copy Link'})}
|
||||
icon='content-copy'
|
||||
type='default'
|
||||
/>
|
||||
}
|
||||
{canShare &&
|
||||
<OptionItem
|
||||
action={onShare}
|
||||
label={intl.formatMessage({id: 'channel_bookmark.share_option', defaultMessage: 'Share'})}
|
||||
icon='share-variant-outline'
|
||||
type='default'
|
||||
/>
|
||||
}
|
||||
{canDeleteBookmarks &&
|
||||
<OptionItem
|
||||
action={onDelete}
|
||||
destructive={true}
|
||||
label={intl.formatMessage({id: 'channel_bookmark.delete_option', defaultMessage: 'Delete'})}
|
||||
icon='trash-can-outline'
|
||||
type='default'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelBookmarkOptions;
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl, type IntlShape} from 'react-intl';
|
||||
import {Alert, StyleSheet} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {ITEM_HEIGHT} from '@components/option_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useGalleryItem} from '@hooks/gallery';
|
||||
import {TITLE_HEIGHT} from '@screens/bottom_sheet';
|
||||
import {bottomSheet, dismissOverlay} from '@screens/navigation';
|
||||
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
|
||||
import {isDocument} from '@utils/file';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {normalizeProtocol, tryOpenURL} from '@utils/url';
|
||||
|
||||
import BookmarkDetails from './bookmark_details';
|
||||
import BookmarkDocument from './bookmark_document';
|
||||
import ChannelBookmarkOptions from './bookmark_options';
|
||||
|
||||
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
import type {GalleryAction} from '@typings/screens/gallery';
|
||||
|
||||
type Props = {
|
||||
bookmark: ChannelBookmarkModel;
|
||||
canDeleteBookmarks: boolean;
|
||||
canDownloadFiles: boolean;
|
||||
canEditBookmarks: boolean;
|
||||
file?: FileModel;
|
||||
galleryIdentifier: string;
|
||||
index?: number;
|
||||
onPress?: (index: number) => void;
|
||||
publicLinkEnabled: boolean;
|
||||
siteURL: string;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 6,
|
||||
height: 48,
|
||||
},
|
||||
});
|
||||
|
||||
const openLink = async (href: string, serverUrl: string, siteURL: string, intl: IntlShape) => {
|
||||
const url = normalizeProtocol(href);
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = matchDeepLink(url, serverUrl, siteURL);
|
||||
|
||||
const onLinkError = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.link.error.title',
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.link.error.text',
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (match) {
|
||||
const {error} = await handleDeepLink(match.url, intl);
|
||||
if (error) {
|
||||
tryOpenURL(match.url, onLinkError);
|
||||
}
|
||||
} else {
|
||||
tryOpenURL(url, onLinkError);
|
||||
}
|
||||
};
|
||||
|
||||
const ChannelBookmark = ({
|
||||
bookmark, canDeleteBookmarks, canDownloadFiles, canEditBookmarks,
|
||||
file, galleryIdentifier, index, onPress, publicLinkEnabled, siteURL,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
const serverUrl = useServerUrl();
|
||||
const intl = useIntl();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const [action, setAction] = useState<GalleryAction>('none');
|
||||
const isDocumentFile = useMemo(() => isDocument(file), [file]);
|
||||
const canCopyPublicLink = Boolean((bookmark.type === 'link' || (file?.id && publicLinkEnabled)) && managedConfig.copyAndPasteProtection !== 'true');
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (bookmark.linkUrl) {
|
||||
openLink(bookmark.linkUrl, siteURL, serverUrl, intl);
|
||||
return;
|
||||
}
|
||||
|
||||
onPress?.(index || 0);
|
||||
}, [bookmark, index, intl, onPress, serverUrl, siteURL]);
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
const canShare = canDownloadFiles || bookmark.type === 'link';
|
||||
const count = [canCopyPublicLink, canDeleteBookmarks, canShare, canEditBookmarks].
|
||||
filter((e) => e).length;
|
||||
|
||||
const renderContent = () => (
|
||||
<ChannelBookmarkOptions
|
||||
bookmark={bookmark}
|
||||
canCopyPublicLink={canCopyPublicLink}
|
||||
canDeleteBookmarks={canDeleteBookmarks}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
canEditBookmarks={canEditBookmarks}
|
||||
file={file}
|
||||
setAction={setAction}
|
||||
/>
|
||||
);
|
||||
|
||||
bottomSheet({
|
||||
title: bookmark.displayName,
|
||||
renderContent,
|
||||
snapPoints: [1, bottomSheetSnapPoint(1, (count * ITEM_HEIGHT), bottom) + TITLE_HEIGHT],
|
||||
theme,
|
||||
closeButtonId: 'close-channel-bookmark-actions',
|
||||
});
|
||||
}, [bookmark, bottom, canCopyPublicLink, canDeleteBookmarks, canDownloadFiles, canEditBookmarks, file, theme]);
|
||||
|
||||
const {onGestureEvent, ref} = useGalleryItem(galleryIdentifier, index || 0, handlePress);
|
||||
|
||||
useEffect(() => {
|
||||
if (action === 'none' && bookmark.id) {
|
||||
dismissOverlay(bookmark.id);
|
||||
}
|
||||
}, [action]);
|
||||
|
||||
if (isDocumentFile) {
|
||||
return (
|
||||
<BookmarkDocument
|
||||
bookmark={bookmark}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={file!}
|
||||
onLongPress={handleLongPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
containerStyle={styles.container}
|
||||
onPress={onGestureEvent}
|
||||
onLongPress={handleLongPress}
|
||||
ref={ref}
|
||||
>
|
||||
<BookmarkDetails
|
||||
bookmark={bookmark}
|
||||
file={file}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelBookmark;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import {switchMap} from 'rxjs';
|
||||
|
||||
import {observeFileById} from '@queries/servers/file';
|
||||
import {observeConfigValue} from '@queries/servers/system';
|
||||
|
||||
import ChannelBookmark from './channel_bookmark';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
bookmark: ChannelBookmarkModel;
|
||||
}
|
||||
|
||||
const enhanced = withObservables([], ({bookmark, database}: Props) => {
|
||||
const observed = bookmark.observe();
|
||||
const file = observed.pipe(
|
||||
switchMap((b) => observeFileById(database, b.fileId || '')),
|
||||
);
|
||||
|
||||
return {
|
||||
bookmark: observed,
|
||||
file,
|
||||
siteURL: observeConfigValue(database, 'SiteURL'),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ChannelBookmark));
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {FlatList, View, type ListRenderItemInfo, type NativeSyntheticEvent, type NativeScrollEvent} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
import {GalleryInit} from '@context/gallery';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useChannelBookmarkFiles} from '@hooks/files';
|
||||
import ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import AddBookmark from './add_bookmark';
|
||||
import ChannelBookmark from './channel_bookmark';
|
||||
|
||||
type Props = {
|
||||
bookmarks: ChannelBookmarkModel[];
|
||||
canAddBookmarks: boolean;
|
||||
canDeleteBookmarks: boolean;
|
||||
canDownloadFiles: boolean;
|
||||
canEditBookmarks: boolean;
|
||||
canUploadFiles: boolean;
|
||||
channelId: string;
|
||||
currentUserId: string;
|
||||
publicLinkEnabled: boolean;
|
||||
showInInfo: boolean;
|
||||
separator?: boolean;
|
||||
}
|
||||
|
||||
const GRADIENT_LOCATIONS = [0, 0.64, 1];
|
||||
const SCROLL_OFFSET = 10;
|
||||
|
||||
const isCloseToBottom = ({layoutMeasurement, contentOffset, contentSize}: NativeScrollEvent) => {
|
||||
return layoutMeasurement.width + contentOffset.x <= contentSize.width - SCROLL_OFFSET;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
marginTop: 8,
|
||||
marginBottom: 24,
|
||||
},
|
||||
emptyItemSeparator: {
|
||||
width: 20,
|
||||
},
|
||||
addContainer: {
|
||||
width: 40,
|
||||
alignContent: 'center',
|
||||
},
|
||||
gradient: {
|
||||
height: 48,
|
||||
width: 68,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
},
|
||||
channelView: {
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
}));
|
||||
|
||||
const ChannelBookmarks = ({
|
||||
bookmarks, canAddBookmarks, canDeleteBookmarks, canDownloadFiles, canEditBookmarks, canUploadFiles,
|
||||
channelId, currentUserId, publicLinkEnabled, showInInfo, separator = true,
|
||||
}: Props) => {
|
||||
const galleryIdentifier = `${channelId}-bookmarks`;
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const files = useChannelBookmarkFiles(bookmarks, publicLinkEnabled);
|
||||
const [allowEndFade, setAllowEndFade] = useState(true);
|
||||
|
||||
const attachmentIndex = useCallback((fileId: string) => {
|
||||
return files.findIndex((file) => file.id === fileId) || 0;
|
||||
}, [files]);
|
||||
|
||||
const handlePreviewPress = useCallback(preventDoubleTap((idx: number) => {
|
||||
if (files.length) {
|
||||
const items = files.map((f) => fileToGalleryItem(f, f.user_id));
|
||||
openGalleryAtIndex(galleryIdentifier, idx, items);
|
||||
}
|
||||
}), [files]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<ChannelBookmarkModel>) => {
|
||||
return (
|
||||
<ChannelBookmark
|
||||
bookmark={item}
|
||||
canDeleteBookmarks={canDeleteBookmarks}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
canEditBookmarks={canEditBookmarks}
|
||||
galleryIdentifier={galleryIdentifier}
|
||||
index={item.fileId ? attachmentIndex(item.fileId) : undefined}
|
||||
onPress={handlePreviewPress}
|
||||
publicLinkEnabled={publicLinkEnabled}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
attachmentIndex, bookmarks, canDownloadFiles, canDeleteBookmarks, canEditBookmarks,
|
||||
handlePreviewPress, publicLinkEnabled,
|
||||
]);
|
||||
|
||||
const renderItemSeparator = useCallback(() => (<View style={styles.emptyItemSeparator}/>), []);
|
||||
|
||||
const onScrolled = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
setAllowEndFade(isCloseToBottom(e.nativeEvent));
|
||||
}, []);
|
||||
|
||||
const gradientColors = useMemo(() => [
|
||||
theme.centerChannelBg,
|
||||
changeOpacity(theme.centerChannelBg, 0.6458),
|
||||
changeOpacity(theme.centerChannelBg, 0),
|
||||
], [theme]);
|
||||
|
||||
if (!bookmarks.length && showInInfo && canAddBookmarks) {
|
||||
return (
|
||||
<AddBookmark
|
||||
bookmarksCount={0}
|
||||
canUploadFiles={canUploadFiles}
|
||||
channelId={channelId}
|
||||
currentUserId={currentUserId}
|
||||
showLarge={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (bookmarks.length) {
|
||||
return (
|
||||
<GalleryInit galleryIdentifier={galleryIdentifier}>
|
||||
<Animated.View>
|
||||
<Animated.View style={[styles.container, showInInfo ? undefined : styles.channelView]}>
|
||||
<FlatList
|
||||
bounces={true}
|
||||
alwaysBounceHorizontal={false}
|
||||
data={bookmarks}
|
||||
horizontal={true}
|
||||
renderItem={renderItem}
|
||||
ItemSeparatorComponent={renderItemSeparator}
|
||||
onScroll={onScrolled}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{height: 48, alignItems: 'center'}}
|
||||
/>
|
||||
{canAddBookmarks &&
|
||||
<View style={styles.addContainer}>
|
||||
{allowEndFade &&
|
||||
<LinearGradient
|
||||
angle={290}
|
||||
useAngle={true}
|
||||
locations={GRADIENT_LOCATIONS}
|
||||
colors={gradientColors}
|
||||
style={styles.gradient}
|
||||
pointerEvents={'none'}
|
||||
/>
|
||||
}
|
||||
<AddBookmark
|
||||
bookmarksCount={bookmarks.length}
|
||||
canUploadFiles={canUploadFiles}
|
||||
channelId={channelId}
|
||||
currentUserId={currentUserId}
|
||||
showLarge={false}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</Animated.View>
|
||||
{separator &&
|
||||
<View style={styles.separator}/>
|
||||
}
|
||||
</Animated.View>
|
||||
</GalleryInit>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ChannelBookmarks;
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
|
||||
import {observeBookmarks, observeCanDeleteBookmarks, observeCanEditBookmarks} from '@queries/servers/channel_bookmark';
|
||||
import {observeCanDownloadFiles, observeCanUploadFiles, observeConfigBooleanValue, observeCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import ChannelBookmarks from './channel_bookmarks';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type Props = WithDatabaseArgs & {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const enhanced = withObservables([], ({channelId, database}: Props) => {
|
||||
return {
|
||||
bookmarks: observeBookmarks(database, channelId),
|
||||
canDeleteBookmarks: observeCanDeleteBookmarks(database, channelId),
|
||||
canDownloadFiles: observeCanDownloadFiles(database),
|
||||
canEditBookmarks: observeCanEditBookmarks(database, channelId),
|
||||
canUploadFiles: observeCanUploadFiles(database),
|
||||
currentUserId: observeCurrentUserId(database),
|
||||
publicLinkEnabled: observeConfigBooleanValue(database, 'EnablePublicLink'),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(ChannelBookmarks));
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {forwardRef, useImperativeHandle, useRef, useState, type ReactNode, useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform, StatusBar, type StatusBarStyle} from 'react-native';
|
||||
import FileViewer from 'react-native-file-viewer';
|
||||
import FileSystem from 'react-native-fs';
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import {downloadFile} from '@actions/remote/file';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {alertDownloadDocumentDisabled, alertDownloadFailed, alertFailedToOpenDocument} from '@utils/document';
|
||||
import {getFullErrorMessage, isErrorWithMessage} from '@utils/errors';
|
||||
import {fileExists, getLocalFilePathFromFile} from '@utils/file';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import type {ClientResponse, ProgressPromise} from '@mattermost/react-native-network-client';
|
||||
|
||||
export type DocumentRef = {
|
||||
handlePreviewPress: () => void;
|
||||
}
|
||||
|
||||
type DocumentProps = {
|
||||
canDownloadFiles: boolean;
|
||||
file: FileInfo;
|
||||
children: ReactNode;
|
||||
onProgress: (progress: number) => void;
|
||||
}
|
||||
|
||||
const Document = forwardRef<DocumentRef, DocumentProps>(({canDownloadFiles, children, onProgress, file}: DocumentProps, ref) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const [didCancel, setDidCancel] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [preview, setPreview] = useState(false);
|
||||
const downloadTask = useRef<ProgressPromise<ClientResponse>>();
|
||||
|
||||
const cancelDownload = () => {
|
||||
setDidCancel(true);
|
||||
if (downloadTask.current?.cancel) {
|
||||
downloadTask.current.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAndPreviewFile = useCallback(async () => {
|
||||
setDidCancel(false);
|
||||
let path;
|
||||
let exists = false;
|
||||
|
||||
try {
|
||||
path = decodeURIComponent(file.localPath || '');
|
||||
if (path) {
|
||||
exists = await fileExists(path);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
path = getLocalFilePathFromFile(serverUrl, file);
|
||||
exists = await fileExists(path);
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
openDocument();
|
||||
} else {
|
||||
setDownloading(true);
|
||||
downloadTask.current = downloadFile(serverUrl, file.id!, path!);
|
||||
downloadTask.current?.progress?.(onProgress);
|
||||
|
||||
await downloadTask.current;
|
||||
onProgress(1);
|
||||
openDocument();
|
||||
}
|
||||
} catch (error) {
|
||||
if (path) {
|
||||
FileSystem.unlink(path).catch(emptyFunction);
|
||||
}
|
||||
setDownloading(false);
|
||||
onProgress(0);
|
||||
|
||||
if (!isErrorWithMessage(error) || error.message !== 'cancelled') {
|
||||
logDebug('error on downloadAndPreviewFile', getFullErrorMessage(error));
|
||||
alertDownloadFailed(intl);
|
||||
}
|
||||
}
|
||||
}, [file, onProgress]);
|
||||
|
||||
const setStatusBarColor = useCallback((style: StatusBarStyle = 'light-content') => {
|
||||
if (Platform.OS === 'ios') {
|
||||
if (style) {
|
||||
StatusBar.setBarStyle(style, true);
|
||||
} else {
|
||||
const headerColor = tinyColor(theme.sidebarHeaderBg);
|
||||
let barStyle: StatusBarStyle = 'light-content';
|
||||
if (headerColor.isLight() && Platform.OS === 'ios') {
|
||||
barStyle = 'dark-content';
|
||||
}
|
||||
StatusBar.setBarStyle(barStyle, true);
|
||||
}
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const openDocument = useCallback(async () => {
|
||||
if (!didCancel && !preview) {
|
||||
let path = decodeURIComponent(file.localPath || '');
|
||||
let exists = false;
|
||||
if (path) {
|
||||
exists = await fileExists(path);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
path = getLocalFilePathFromFile(serverUrl, file);
|
||||
}
|
||||
|
||||
setPreview(true);
|
||||
setStatusBarColor('dark-content');
|
||||
FileViewer.open(path!.replace('file://', ''), {
|
||||
displayName: decodeURIComponent(file.name),
|
||||
onDismiss: onDonePreviewingFile,
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
}).then(() => {
|
||||
setDownloading(false);
|
||||
onProgress(0);
|
||||
}).catch(() => {
|
||||
alertFailedToOpenDocument(file, intl);
|
||||
onDonePreviewingFile();
|
||||
|
||||
if (path) {
|
||||
FileSystem.unlink(path).catch(emptyFunction);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [didCancel, preview, file, onProgress, setStatusBarColor]);
|
||||
|
||||
const handlePreviewPress = useCallback(async () => {
|
||||
if (!canDownloadFiles) {
|
||||
alertDownloadDocumentDisabled(intl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloading) {
|
||||
onProgress(0);
|
||||
cancelDownload();
|
||||
setDownloading(false);
|
||||
} else {
|
||||
downloadAndPreviewFile();
|
||||
}
|
||||
}, [canDownloadFiles, downloadAndPreviewFile, downloading, intl, onProgress, openDocument]);
|
||||
|
||||
const onDonePreviewingFile = () => {
|
||||
onProgress(0);
|
||||
setDownloading(false);
|
||||
setPreview(false);
|
||||
setStatusBarColor();
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handlePreviewPress,
|
||||
}), [handlePreviewPress]);
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
Document.displayName = 'Document';
|
||||
|
||||
export default Document;
|
||||
|
|
@ -2,17 +2,34 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {forwardRef, useImperativeHandle, useRef, useState} from 'react';
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform, StatusBar, type StatusBarStyle, StyleSheet, TouchableOpacity, View} from 'react-native';
|
||||
import FileViewer from 'react-native-file-viewer';
|
||||
import FileSystem from 'react-native-fs';
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import Document, {type DocumentRef} from '@components/document';
|
||||
import ProgressBar from '@components/progress_bar';
|
||||
import {DOWNLOAD_TIMEOUT} from '@constants/network';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {alertDownloadDocumentDisabled, alertDownloadFailed, alertFailedToOpenDocument} from '@utils/document';
|
||||
import {getFullErrorMessage, isErrorWithMessage} from '@utils/errors';
|
||||
import {fileExists, getLocalFilePathFromFile} from '@utils/file';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import FileIcon from './file_icon';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type {ClientResponse, ProgressPromise} from '@mattermost/react-native-network-client';
|
||||
|
||||
export type DocumentFileRef = {
|
||||
handlePreviewPress: () => void;
|
||||
}
|
||||
|
||||
type DocumentFileProps = {
|
||||
backgroundColor?: string;
|
||||
disabled?: boolean;
|
||||
canDownloadFiles: boolean;
|
||||
file: FileInfo;
|
||||
}
|
||||
|
|
@ -27,13 +44,122 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const DocumentFile = forwardRef<DocumentRef, DocumentFileProps>(({backgroundColor, canDownloadFiles, disabled = false, file}: DocumentFileProps, ref) => {
|
||||
const DocumentFile = forwardRef<DocumentFileRef, DocumentFileProps>(({backgroundColor, canDownloadFiles, file}: DocumentFileProps, ref) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const [didCancel, setDidCancel] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [preview, setPreview] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const document = useRef<DocumentRef>(null);
|
||||
let client: Client | undefined;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
const downloadTask = useRef<ProgressPromise<ClientResponse>>();
|
||||
|
||||
const cancelDownload = () => {
|
||||
setDidCancel(true);
|
||||
if (downloadTask.current?.cancel) {
|
||||
downloadTask.current.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAndPreviewFile = async () => {
|
||||
setDidCancel(false);
|
||||
let path;
|
||||
|
||||
try {
|
||||
path = getLocalFilePathFromFile(serverUrl, file);
|
||||
const exists = await fileExists(path);
|
||||
if (exists) {
|
||||
openDocument();
|
||||
} else {
|
||||
setDownloading(true);
|
||||
downloadTask.current = client?.apiClient.download(client?.getFileRoute(file.id!), path!.replace('file://', ''), {timeoutInterval: DOWNLOAD_TIMEOUT});
|
||||
downloadTask.current?.progress?.(setProgress);
|
||||
|
||||
await downloadTask.current;
|
||||
setProgress(1);
|
||||
openDocument();
|
||||
}
|
||||
} catch (error) {
|
||||
if (path) {
|
||||
FileSystem.unlink(path).catch(emptyFunction);
|
||||
}
|
||||
setDownloading(false);
|
||||
setProgress(0);
|
||||
|
||||
if (!isErrorWithMessage(error) || error.message !== 'cancelled') {
|
||||
logDebug('error on downloadAndPreviewFile', getFullErrorMessage(error));
|
||||
alertDownloadFailed(intl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviewPress = async () => {
|
||||
document.current?.handlePreviewPress();
|
||||
if (!canDownloadFiles) {
|
||||
alertDownloadDocumentDisabled(intl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloading && progress < 1) {
|
||||
cancelDownload();
|
||||
} else if (downloading) {
|
||||
setProgress(0);
|
||||
setDidCancel(true);
|
||||
setDownloading(false);
|
||||
} else {
|
||||
downloadAndPreviewFile();
|
||||
}
|
||||
};
|
||||
|
||||
const onDonePreviewingFile = () => {
|
||||
setProgress(0);
|
||||
setDownloading(false);
|
||||
setPreview(false);
|
||||
setStatusBarColor();
|
||||
};
|
||||
|
||||
const openDocument = () => {
|
||||
if (!didCancel && !preview) {
|
||||
const path = getLocalFilePathFromFile(serverUrl, file);
|
||||
setPreview(true);
|
||||
setStatusBarColor('dark-content');
|
||||
FileViewer.open(path!, {
|
||||
displayName: file.name,
|
||||
onDismiss: onDonePreviewingFile,
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true,
|
||||
}).then(() => {
|
||||
setDownloading(false);
|
||||
setProgress(0);
|
||||
}).catch(() => {
|
||||
alertFailedToOpenDocument(file, intl);
|
||||
onDonePreviewingFile();
|
||||
|
||||
if (path) {
|
||||
FileSystem.unlink(path).catch(emptyFunction);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setStatusBarColor = (style: StatusBarStyle = 'light-content') => {
|
||||
if (Platform.OS === 'ios') {
|
||||
if (style) {
|
||||
StatusBar.setBarStyle(style, true);
|
||||
} else {
|
||||
const headerColor = tinyColor(theme.sidebarHeaderBg);
|
||||
let barStyle: StatusBarStyle = 'light-content';
|
||||
if (headerColor.isLight() && Platform.OS === 'ios') {
|
||||
barStyle = 'dark-content';
|
||||
}
|
||||
StatusBar.setBarStyle(barStyle, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
|
@ -48,13 +174,13 @@ const DocumentFile = forwardRef<DocumentRef, DocumentFileProps>(({backgroundColo
|
|||
);
|
||||
|
||||
let fileAttachmentComponent = icon;
|
||||
if (progress) {
|
||||
if (downloading) {
|
||||
fileAttachmentComponent = (
|
||||
<>
|
||||
{icon}
|
||||
<View style={[StyleSheet.absoluteFill, styles.progress]}>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
progress={progress || 0.1}
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -63,19 +189,9 @@ const DocumentFile = forwardRef<DocumentRef, DocumentFileProps>(({backgroundColo
|
|||
}
|
||||
|
||||
return (
|
||||
<Document
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={file}
|
||||
onProgress={setProgress}
|
||||
ref={document}
|
||||
>
|
||||
<TouchableOpacity
|
||||
disabled={disabled}
|
||||
onPress={handlePreviewPress}
|
||||
>
|
||||
{fileAttachmentComponent}
|
||||
</TouchableOpacity>
|
||||
</Document>
|
||||
<TouchableOpacity onPress={handlePreviewPress}>
|
||||
{fileAttachmentComponent}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {useGalleryItem} from '@hooks/gallery';
|
|||
import {isDocument, isImage, isVideo} from '@utils/file';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import DocumentFile from './document_file';
|
||||
import DocumentFile, {type DocumentFileRef} from './document_file';
|
||||
import FileIcon from './file_icon';
|
||||
import FileInfo from './file_info';
|
||||
import FileOptionsIcon from './file_options_icon';
|
||||
|
|
@ -19,9 +19,6 @@ import ImageFile from './image_file';
|
|||
import ImageFileOverlay from './image_file_overlay';
|
||||
import VideoFile from './video_file';
|
||||
|
||||
import type {DocumentRef} from '@components/document';
|
||||
import type {ResizeMode} from 'react-native-fast-image';
|
||||
|
||||
type FileProps = {
|
||||
canDownloadFiles: boolean;
|
||||
file: FileInfo;
|
||||
|
|
@ -39,8 +36,6 @@ type FileProps = {
|
|||
showDate?: boolean;
|
||||
updateFileForGallery: (idx: number, file: FileInfo) => void;
|
||||
asCard?: boolean;
|
||||
resizeMode?: ResizeMode;
|
||||
isPressDisabled?: boolean;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
|
@ -84,10 +79,8 @@ const File = ({
|
|||
showDate = false,
|
||||
updateFileForGallery,
|
||||
wrapperWidth = 300,
|
||||
resizeMode = 'cover',
|
||||
isPressDisabled = false,
|
||||
}: FileProps) => {
|
||||
const document = useRef<DocumentRef>(null);
|
||||
const document = useRef<DocumentFileRef>(null);
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
|
|
@ -108,7 +101,6 @@ const File = ({
|
|||
const renderCardWithImage = (fileIcon: JSX.Element) => {
|
||||
const fileInfo = (
|
||||
<FileInfo
|
||||
disabled={isPressDisabled}
|
||||
file={file}
|
||||
showDate={showDate}
|
||||
channelName={channelName}
|
||||
|
|
@ -135,17 +127,14 @@ const File = ({
|
|||
let fileComponent;
|
||||
if (isVideo(file) && publicLinkEnabled) {
|
||||
const renderVideoFile = (
|
||||
<TouchableWithoutFeedback
|
||||
disabled={isPressDisabled}
|
||||
onPress={onGestureEvent}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={onGestureEvent}>
|
||||
<Animated.View style={[styles, asCard ? style.imageVideo : null]}>
|
||||
<VideoFile
|
||||
file={file}
|
||||
forwardRef={ref}
|
||||
inViewPort={inViewPort}
|
||||
isSingleImage={isSingleImage}
|
||||
resizeMode={resizeMode}
|
||||
resizeMode={'cover'}
|
||||
wrapperWidth={wrapperWidth}
|
||||
updateFileForGallery={updateFileForGallery}
|
||||
index={index}
|
||||
|
|
@ -162,17 +151,14 @@ const File = ({
|
|||
fileComponent = asCard ? renderCardWithImage(renderVideoFile) : renderVideoFile;
|
||||
} else if (isImage(file)) {
|
||||
const renderImageFile = (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onGestureEvent}
|
||||
disabled={isPressDisabled}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={onGestureEvent}>
|
||||
<Animated.View style={[styles, asCard ? style.imageVideo : null]}>
|
||||
<ImageFile
|
||||
file={file}
|
||||
forwardRef={ref}
|
||||
inViewPort={inViewPort}
|
||||
isSingleImage={isSingleImage}
|
||||
resizeMode={resizeMode}
|
||||
resizeMode={'cover'}
|
||||
wrapperWidth={wrapperWidth}
|
||||
/>
|
||||
{Boolean(nonVisibleImagesCount) &&
|
||||
|
|
@ -191,7 +177,6 @@ const File = ({
|
|||
<DocumentFile
|
||||
ref={document}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
disabled={isPressDisabled}
|
||||
file={file}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -199,7 +184,6 @@ const File = ({
|
|||
|
||||
const fileInfo = (
|
||||
<FileInfo
|
||||
disabled={isPressDisabled}
|
||||
file={file}
|
||||
showDate={showDate}
|
||||
channelName={channelName}
|
||||
|
|
@ -223,7 +207,6 @@ const File = ({
|
|||
const touchableWithPreview = (
|
||||
<TouchableWithFeedback
|
||||
onPress={handlePreviewPress}
|
||||
disabled={isPressDisabled}
|
||||
type={'opacity'}
|
||||
>
|
||||
<FileIcon
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ type FileIconProps = {
|
|||
backgroundColor?: string;
|
||||
defaultImage?: boolean;
|
||||
failed?: boolean;
|
||||
file?: FileInfo | ExtractedFileInfo;
|
||||
file?: FileInfo;
|
||||
iconColor?: string;
|
||||
iconSize?: number;
|
||||
smallImage?: boolean;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|||
import {typography} from '@utils/typography';
|
||||
|
||||
type FileInfoProps = {
|
||||
disabled?: boolean;
|
||||
file: FileInfo;
|
||||
showDate: boolean;
|
||||
channelName?: string ;
|
||||
|
|
@ -59,22 +58,18 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const FileInfo = ({disabled, file, channelName, showDate, onPress}: FileInfoProps) => {
|
||||
const FileInfo = ({file, channelName, showDate, onPress}: FileInfoProps) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.attachmentContainer}>
|
||||
<TouchableOpacity
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
>
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={style.fileName}
|
||||
>
|
||||
{decodeURIComponent(file.name.trim())}
|
||||
{file.name.trim()}
|
||||
</Text>
|
||||
<View style={style.fileDownloadContainer}>
|
||||
{channelName &&
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import React, {useCallback, useMemo, useState} from 'react';
|
|||
import {StyleSheet, useWindowDimensions, View} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import {buildFilePreviewUrl, buildFileThumbnailUrl, buildFileUrl} from '@actions/remote/file';
|
||||
import {buildFilePreviewUrl, buildFileThumbnailUrl} from '@actions/remote/file';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProgressiveImage from '@components/progressive_image';
|
||||
import {useServerUrl} from '@context/server';
|
||||
|
|
@ -99,17 +99,12 @@ const ImageFile = ({
|
|||
} else if (file.id) {
|
||||
if (file.mini_preview && file.mime_type) {
|
||||
props.thumbnailUri = `data:${file.mime_type};base64,${file.mini_preview}`;
|
||||
} else if (file.has_preview_image) {
|
||||
} else {
|
||||
props.thumbnailUri = buildFileThumbnailUrl(serverUrl, file.id);
|
||||
}
|
||||
if (file.has_preview_image) {
|
||||
props.imageUri = buildFilePreviewUrl(serverUrl, file.id);
|
||||
} else {
|
||||
props.imageUri = buildFileUrl(serverUrl, file.id, file.update_at);
|
||||
}
|
||||
props.imageUri = buildFilePreviewUrl(serverUrl, file.id);
|
||||
props.inViewPort = inViewPort;
|
||||
}
|
||||
|
||||
return props;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
maxWidth: 315,
|
||||
},
|
||||
readOnly: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
backgroundColor: changeOpacity(theme.centerChannelBg, 0.16),
|
||||
},
|
||||
smallLabel: {
|
||||
fontSize: 10,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import FormattedText from '@components/formatted_text';
|
|||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {Events} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {makeStyleSheetFromTheme, hexToHue, changeOpacity} from '@utils/theme';
|
||||
|
|
@ -116,7 +115,6 @@ const MoreMessages = ({
|
|||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTablet = useIsTablet();
|
||||
const pressed = useRef(false);
|
||||
const resetting = useRef(false);
|
||||
const initialScroll = useRef(false);
|
||||
|
|
@ -128,7 +126,7 @@ const MoreMessages = ({
|
|||
const callsAdjustment = useCallsAdjustment(serverUrl, channelId);
|
||||
|
||||
// The final top:
|
||||
const adjustedTop = (isTablet ? 0 : insets.top) + callsAdjustment;
|
||||
const adjustedTop = insets.top + callsAdjustment;
|
||||
|
||||
const BARS_FACTOR = Math.abs((1) / (HIDDEN_TOP - SHOWN_TOP));
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export const MM_TABLES = {
|
|||
CATEGORY: 'Category',
|
||||
CATEGORY_CHANNEL: 'CategoryChannel',
|
||||
CHANNEL: 'Channel',
|
||||
CHANNEL_BOOKMARK: 'ChannelBookmark',
|
||||
CHANNEL_INFO: 'ChannelInfo',
|
||||
CHANNEL_MEMBERSHIP: 'ChannelMembership',
|
||||
CONFIG: 'Config',
|
||||
|
|
@ -84,7 +83,6 @@ export const GLOBAL_IDENTIFIERS = {
|
|||
LAST_VIEWED_CHANNEL: 'lastViewedChannel',
|
||||
LAST_VIEWED_THREAD: 'lastViewedThread',
|
||||
PUSH_DISABLED_ACK: 'pushDisabledAck',
|
||||
APP_INACTIVE_SINCE: 'appInactiveSince',
|
||||
};
|
||||
|
||||
export enum OperationType {
|
||||
|
|
|
|||
|
|
@ -153,10 +153,4 @@ export default {
|
|||
},
|
||||
MANAGE_BOTS: 'manage_bots',
|
||||
MANAGE_OTHERS_BOTS: 'manage_others_bots',
|
||||
ADD_BOOKMARK_PUBLIC_CHANNEL: 'add_bookmark_public_channel',
|
||||
ADD_BOOKMARK_PRIVATE_CHANNEL: 'add_bookmark_private_channel',
|
||||
EDIT_BOOKMARK_PUBLIC_CHANNEL: 'edit_bookmark_public_channel',
|
||||
EDIT_BOOKMARK_PRIVATE_CHANNEL: 'edit_bookmark_private_channel',
|
||||
DELETE_BOOKMARK_PUBLIC_CHANNEL: 'delete_bookmark_public_channel',
|
||||
DELETE_BOOKMARK_PRIVATE_CHANNEL: 'delete_bookmark_private_channel',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,8 +72,6 @@ export const THREAD = 'Thread';
|
|||
export const THREAD_FOLLOW_BUTTON = 'ThreadFollowButton';
|
||||
export const THREAD_OPTIONS = 'ThreadOptions';
|
||||
export const USER_PROFILE = 'UserProfile';
|
||||
export const CHANNEL_BOOKMARK = 'ChannelBookmarkAddOrEdit';
|
||||
export const GENERIC_OVERLAY = 'GenericOverlay';
|
||||
|
||||
export default {
|
||||
ABOUT,
|
||||
|
|
@ -84,7 +82,6 @@ export default {
|
|||
CALL,
|
||||
CHANNEL,
|
||||
CHANNEL_ADD_MEMBERS,
|
||||
CHANNEL_BOOKMARK,
|
||||
CHANNEL_FILES,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_NOTIFICATION_PREFERENCES,
|
||||
|
|
@ -148,7 +145,6 @@ export default {
|
|||
THREAD_FOLLOW_BUTTON,
|
||||
THREAD_OPTIONS,
|
||||
USER_PROFILE,
|
||||
GENERIC_OVERLAY,
|
||||
} as const;
|
||||
|
||||
export const MODAL_SCREENS_WITHOUT_BACK = new Set<string>([
|
||||
|
|
@ -172,7 +168,6 @@ export const SCREENS_WITH_TRANSPARENT_BACKGROUND = new Set<string>([
|
|||
PERMALINK,
|
||||
REVIEW_APP,
|
||||
SNACK_BAR,
|
||||
GENERIC_OVERLAY,
|
||||
]);
|
||||
|
||||
export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export const SNACK_BAR_TYPE = keyMirror({
|
|||
FOLLOW_THREAD: null,
|
||||
INFO_COPIED: null,
|
||||
LINK_COPIED: null,
|
||||
LINK_COPY_FAILED: null,
|
||||
MESSAGE_COPIED: null,
|
||||
MUTE_CHANNEL: null,
|
||||
REMOVE_CHANNEL_USER: null,
|
||||
|
|
@ -66,13 +65,6 @@ export const SNACK_BAR_CONFIG: Record<string, SnackBarConfig> = {
|
|||
canUndo: false,
|
||||
type: MESSAGE_TYPE.SUCCESS,
|
||||
},
|
||||
LINK_COPY_FAILED: {
|
||||
id: t('gallery.copy_link.failed'),
|
||||
defaultMessage: 'Failed to copy link to clipboard',
|
||||
iconName: 'link-variant',
|
||||
canUndo: false,
|
||||
type: MESSAGE_TYPE.ERROR,
|
||||
},
|
||||
MESSAGE_COPIED: {
|
||||
id: t('snack.bar.message.copied'),
|
||||
defaultMessage: 'Text copied to clipboard',
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export const CALL_ERROR_BAR_HEIGHT = 52;
|
|||
export const CALL_NOTIFICATION_BAR_HEIGHT = 40;
|
||||
|
||||
export const ANNOUNCEMENT_BAR_HEIGHT = 40;
|
||||
export const BOOKMARKS_BAR_HEIGHT = 48;
|
||||
|
||||
export const HOME_PADDING = {
|
||||
paddingLeft: 18,
|
||||
|
|
|
|||
|
|
@ -91,9 +91,5 @@ const WebsocketEvents = {
|
|||
GROUP_DISSOCIATED_TO_TEAM: 'received_group_not_associated_to_team',
|
||||
GROUP_ASSOCIATED_TO_CHANNEL: 'received_group_associated_to_channel',
|
||||
GROUP_DISSOCIATED_TO_CHANNEL: 'received_group_not_associated_to_channel',
|
||||
CHANNEL_BOOKMARK_CREATED: 'channel_bookmark_created',
|
||||
CHANNEL_BOOKMARK_UPDATED: 'channel_bookmark_updated',
|
||||
CHANNEL_BOOKMARK_SORTED: 'channel_bookmark_sorted',
|
||||
CHANNEL_BOOKMARK_DELETED: 'channel_bookmark_deleted',
|
||||
};
|
||||
export default WebsocketEvents;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {DatabaseType, MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
|
|||
import AppDatabaseMigrations from '@database/migration/app';
|
||||
import ServerDatabaseMigrations from '@database/migration/server';
|
||||
import {InfoModel, GlobalModel, ServersModel} from '@database/models/app';
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
@ -47,7 +47,7 @@ class DatabaseManager {
|
|||
constructor() {
|
||||
this.appModels = [InfoModel, GlobalModel, ServersModel];
|
||||
this.serverModels = [
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {DatabaseType, MIGRATION_EVENTS, MM_TABLES} from '@constants/database';
|
|||
import AppDatabaseMigrations from '@database/migration/app';
|
||||
import ServerDatabaseMigrations from '@database/migration/server';
|
||||
import {InfoModel, GlobalModel, ServersModel} from '@database/models/app';
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
import {CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
@ -45,7 +45,7 @@ class DatabaseManager {
|
|||
constructor() {
|
||||
this.appModels = [InfoModel, GlobalModel, ServersModel];
|
||||
this.serverModels = [
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelBookmarkModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
CategoryModel, CategoryChannelModel, ChannelModel, ChannelInfoModel, ChannelMembershipModel, ConfigModel, CustomEmojiModel, DraftModel, FileModel,
|
||||
GroupModel, GroupChannelModel, GroupTeamModel, GroupMembershipModel, MyChannelModel, MyChannelSettingsModel, MyTeamModel,
|
||||
PostModel, PostsInChannelModel, PostsInThreadModel, PreferenceModel, ReactionModel, RoleModel,
|
||||
SystemModel, TeamModel, TeamChannelHistoryModel, TeamMembershipModel, TeamSearchHistoryModel,
|
||||
|
|
|
|||
|
|
@ -4,37 +4,13 @@
|
|||
// NOTE : To implement migration, please follow this document
|
||||
// https://nozbe.github.io/WatermelonDB/Advanced/Migrations.html
|
||||
|
||||
import {addColumns, createTable, schemaMigrations} from '@nozbe/watermelondb/Schema/migrations';
|
||||
import {addColumns, schemaMigrations} from '@nozbe/watermelondb/Schema/migrations';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
const {CHANNEL_BOOKMARK, CHANNEL_INFO, DRAFT, POST} = MM_TABLES.SERVER;
|
||||
const {CHANNEL_INFO, DRAFT, POST} = MM_TABLES.SERVER;
|
||||
|
||||
export default schemaMigrations({migrations: [
|
||||
{
|
||||
toVersion: 4,
|
||||
steps: [
|
||||
createTable({
|
||||
name: CHANNEL_BOOKMARK,
|
||||
columns: [
|
||||
{name: 'create_at', type: 'number'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'delete_at', type: 'number'},
|
||||
{name: 'channel_id', type: 'string', isIndexed: true},
|
||||
{name: 'owner_id', type: 'string'},
|
||||
{name: 'file_id', type: 'string', isOptional: true},
|
||||
{name: 'display_name', type: 'string'},
|
||||
{name: 'sort_order', type: 'number'},
|
||||
{name: 'link_url', type: 'string', isOptional: true},
|
||||
{name: 'image_url', type: 'string', isOptional: true},
|
||||
{name: 'emoji', type: 'string', isOptional: true},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'original_id', type: 'string', isOptional: true},
|
||||
{name: 'parent_id', type: 'string', isOptional: true},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
toVersion: 3,
|
||||
steps: [
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {MM_TABLES} from '@constants/database';
|
|||
import type {Query, Relation} from '@nozbe/watermelondb';
|
||||
import type CategoryChannelModel from '@typings/database/models/servers/category_channel';
|
||||
import type ChannelModelInterface from '@typings/database/models/servers/channel';
|
||||
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import type ChannelInfoModel from '@typings/database/models/servers/channel_info';
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
import type DraftModel from '@typings/database/models/servers/draft';
|
||||
|
|
@ -22,7 +21,6 @@ import type UserModel from '@typings/database/models/servers/user';
|
|||
const {
|
||||
CATEGORY_CHANNEL,
|
||||
CHANNEL,
|
||||
CHANNEL_BOOKMARK,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_MEMBERSHIP,
|
||||
DRAFT,
|
||||
|
|
@ -43,9 +41,6 @@ export default class ChannelModel extends Model implements ChannelModelInterface
|
|||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** A CHANNEL can be associated with multiple CHANNEL_BOOKMARK (relationship is 1:N) */
|
||||
[CHANNEL_BOOKMARK]: {type: 'has_many', foreignKey: 'channel_id'},
|
||||
|
||||
/** A CHANNEL can be associated with multiple CHANNEL_MEMBERSHIP (relationship is 1:N) */
|
||||
[CHANNEL_MEMBERSHIP]: {type: 'has_many', foreignKey: 'channel_id'},
|
||||
|
||||
|
|
@ -114,9 +109,6 @@ export default class ChannelModel extends Model implements ChannelModelInterface
|
|||
/** drafts : All drafts for this channel */
|
||||
@children(DRAFT) drafts!: Query<DraftModel>;
|
||||
|
||||
/** bookmarks : All bookmarks for this channel */
|
||||
@children(CHANNEL_BOOKMARK) bookmarks!: Query<ChannelBookmarkModel>;
|
||||
|
||||
/** posts : All posts made in that channel */
|
||||
@children(POST) posts!: Query<PostModel>;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,115 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {type Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import type {Relation} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type ChannelBookmarkInterface from '@typings/database/models/servers/channel_bookmark';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
const {
|
||||
CHANNEL,
|
||||
CHANNEL_BOOKMARK,
|
||||
FILE,
|
||||
USER,
|
||||
} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* The Channel model represents a channel in the Mattermost app.
|
||||
*/
|
||||
export default class ChannelBookmarkModel extends Model implements ChannelBookmarkInterface {
|
||||
/** table (name) : Channel */
|
||||
static table = CHANNEL_BOOKMARK;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** A CHANNEL can be associated to CHANNEL_BOOKMARK (relationship is 1:N) */
|
||||
[CHANNEL]: {type: 'belongs_to', key: 'channel_id'},
|
||||
|
||||
/** A USER can create multiple CHANNEL_BOOKMARK (relationship is 1:N) */
|
||||
[USER]: {type: 'belongs_to', key: 'owner_id'},
|
||||
|
||||
/** A FILE is associated with one CHANNEL_BOOKMARK**/
|
||||
[FILE]: {type: 'has_many', foreignKey: 'file_id'},
|
||||
|
||||
};
|
||||
|
||||
/** create_at : The creation date for this channel bookmark */
|
||||
@field('create_at') createAt!: number;
|
||||
|
||||
/** update_at : The timestamp to when this channel bookmark was last updated on the server */
|
||||
@field('update_at') updateAt!: number;
|
||||
|
||||
/** delete_at : The deletion/archived date of this channel bookmark */
|
||||
@field('delete_at') deleteAt!: number;
|
||||
|
||||
/** channel_id : The channel to which this bookmarks belongs */
|
||||
@field('channel_id') channelId!: string;
|
||||
|
||||
/** owner_id : The user who created this channel bookmark */
|
||||
@field('owner_id') ownerId!: string;
|
||||
|
||||
/** file_id : The file attached this channel bookmark */
|
||||
@field('file_id') fileId?: string;
|
||||
|
||||
/** display_name : The channel bookmark display name (e.g. Important document ) */
|
||||
@field('display_name') displayName!: string;
|
||||
|
||||
/** sort_order : the order in which the bookmark is displayed in the UI. */
|
||||
@field('sort_order') sortOrder!: number;
|
||||
|
||||
/** link_url : The channel bookmark url if of type link */
|
||||
@field('link_url') linkUrl?: string;
|
||||
|
||||
/** image_url : The channel bookmark image url if of type link (optional) */
|
||||
@field('image_url') imageUrl?: string;
|
||||
|
||||
/** emoji : The channel bookmark emoji (optional) */
|
||||
@field('emoji') emoji?: string;
|
||||
|
||||
/** type : The channel bookmark type it can be link or file */
|
||||
@field('type') type!: ChannelBookmarkType;
|
||||
|
||||
/** original_id : The channel bookmark original identifier before it was edited */
|
||||
@field('original_id') originalId?: string;
|
||||
|
||||
/** parent_id : The channel bookmark parent in case is nested */
|
||||
@field('parent_id') parentId?: string;
|
||||
|
||||
/** channel : The CHANNEL to which this CHANNEL_BOOKMARK belongs */
|
||||
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<ChannelModel>;
|
||||
|
||||
/** creator : The USER who created this CHANNEL_BOOKMARK */
|
||||
@immutableRelation(USER, 'owner_id') owner!: Relation<UserModel>;
|
||||
|
||||
/** file : The FILE attached to this CHANNEL_BOOKMARK */
|
||||
@immutableRelation(FILE, 'file_id') file!: Relation<FileModel>;
|
||||
|
||||
toApi = () => {
|
||||
const b: ChannelBookmark = {
|
||||
id: this.id,
|
||||
create_at: this.createAt,
|
||||
update_at: this.updateAt,
|
||||
delete_at: this.deleteAt,
|
||||
channel_id: this.channelId,
|
||||
owner_id: this.ownerId,
|
||||
file_id: this.fileId,
|
||||
display_name: this.displayName,
|
||||
sort_order: this.sortOrder,
|
||||
link_url: this.linkUrl,
|
||||
image_url: this.imageUrl,
|
||||
emoji: this.emoji,
|
||||
type: this.type,
|
||||
original_id: this.originalId,
|
||||
parent_id: this.parentId,
|
||||
};
|
||||
|
||||
return b;
|
||||
};
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import type {Relation} from '@nozbe/watermelondb';
|
|||
import type FileModelInterface from '@typings/database/models/servers/file';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const {CHANNEL_BOOKMARK, FILE, POST} = MM_TABLES.SERVER;
|
||||
const {FILE, POST} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* The File model works in pair with the Post model. It hosts information about the files attached to a Post
|
||||
|
|
@ -22,9 +22,6 @@ export default class FileModel extends Model implements FileModelInterface {
|
|||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** A CHANNEL_BOOKMARK has a 1:1 relationship with FILE. */
|
||||
[CHANNEL_BOOKMARK]: {type: 'has_many', foreignKey: 'file_id'},
|
||||
|
||||
/** A POST has a 1:N relationship with FILE. */
|
||||
[POST]: {type: 'belongs_to', key: 'post_id'},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
export {default as CategoryModel} from './category';
|
||||
export {default as CategoryChannelModel} from './category_channel';
|
||||
export {default as ChannelModel} from './channel';
|
||||
export {default as ChannelBookmarkModel} from './channel_bookmark';
|
||||
export {default as ChannelInfoModel} from './channel_info';
|
||||
export {default as ChannelMembershipModel} from './channel_membership';
|
||||
export {default as ChannelModel} from './channel';
|
||||
export {default as ConfigModel} from './config';
|
||||
export {default as CustomEmojiModel} from './custom_emoji';
|
||||
export {default as DraftModel} from './draft';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import type {UserMentionKey, HighlightWithoutNotificationKey} from '@typings/glo
|
|||
|
||||
const {
|
||||
CHANNEL,
|
||||
CHANNEL_BOOKMARK,
|
||||
CHANNEL_MEMBERSHIP,
|
||||
POST,
|
||||
PREFERENCE,
|
||||
|
|
@ -44,9 +43,6 @@ export default class UserModel extends Model implements UserModelInterface {
|
|||
/** USER has a 1:N relationship with CHANNEL. A user can create multiple channels */
|
||||
[CHANNEL]: {type: 'has_many', foreignKey: 'creator_id'},
|
||||
|
||||
/** USER has a 1:N relationship with CHANNEL_BOOKMARK. A user can create multiple channels */
|
||||
[CHANNEL_BOOKMARK]: {type: 'has_many', foreignKey: 'owner_id'},
|
||||
|
||||
/** USER has a 1:N relationship with CHANNEL_MEMBERSHIP. A user can be part of multiple channels */
|
||||
[CHANNEL_MEMBERSHIP]: {type: 'has_many', foreignKey: 'user_id'},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Model, Q} from '@nozbe/watermelondb';
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {OperationType} from '@constants/database';
|
||||
import {
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '@database/operator/utils/general';
|
||||
import {logWarning} from '@utils/log';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
import type {
|
||||
HandleRecordsArgs,
|
||||
OperationArgs,
|
||||
|
|
@ -39,11 +40,10 @@ export default class BaseDataOperator {
|
|||
* the same value. Hence, prior to that we query the database and pick only those values that are 'new' from the 'Raw' array.
|
||||
* @param {ProcessRecordsArgs} inputsArg
|
||||
* @param {RawValue[]} inputsArg.createOrUpdateRawValues
|
||||
* @param {RawValue[]} inputsArg.deleteRawValues
|
||||
* @param {string} inputsArg.tableName
|
||||
* @param {string} inputsArg.fieldName
|
||||
* @param {(existing: Model, newElement: RawValue) => boolean} inputsArg.buildKeyRecordBy
|
||||
* @returns {Promise<{ProcessRecordResults<T>}>}
|
||||
* @returns {Promise<{ProcessRecordResults}>}
|
||||
*/
|
||||
processRecords = async <T extends Model>({createOrUpdateRawValues = [], deleteRawValues = [], tableName, buildKeyRecordBy, fieldName, shouldUpdate}: ProcessRecordsArgs): Promise<ProcessRecordResults<T>> => {
|
||||
const getRecords = async (rawValues: RawValue[]) => {
|
||||
|
|
@ -77,7 +77,7 @@ export default class BaseDataOperator {
|
|||
|
||||
// for create or update flow
|
||||
const createOrUpdateRaws = await getRecords(createOrUpdateRawValues);
|
||||
const recordsByKeys = createOrUpdateRaws.reduce((result: Record<string, T>, record) => {
|
||||
const recordsByKeys = createOrUpdateRaws.reduce((result: Record<string, Model>, record) => {
|
||||
// @ts-expect-error object with string key
|
||||
const key = buildKeyRecordBy?.(record) || record[fieldName];
|
||||
result[key] = record;
|
||||
|
|
@ -125,9 +125,9 @@ export default class BaseDataOperator {
|
|||
* @param {string} prepareRecord.tableName
|
||||
* @param {RawValue[]} prepareRecord.createRaws
|
||||
* @param {RawValue[]} prepareRecord.updateRaws
|
||||
* @param {T extends Model[]} prepareRecord.deleteRaws
|
||||
* @param {(TransformerArgs) => Promise<T extends Model>;} transformer
|
||||
* @returns {Promise<T extends Model[]>}
|
||||
* @param {Model[]} prepareRecord.deleteRaws
|
||||
* @param {(TransformerArgs) => Promise<Model>;} transformer
|
||||
* @returns {Promise<Model[]>}
|
||||
*/
|
||||
prepareRecords = async <T extends Model>({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs<T>): Promise<T[]> => {
|
||||
if (!this.database) {
|
||||
|
|
@ -210,7 +210,7 @@ export default class BaseDataOperator {
|
|||
* @returns {Promise<Model[]>}
|
||||
*/
|
||||
async handleRecords<T extends Model>({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues = [], tableName, prepareRecordsOnly = true, shouldUpdate}: HandleRecordsArgs<T>, description: string): Promise<T[]> {
|
||||
if (!createOrUpdateRawValues.length && !deleteRawValues.length) {
|
||||
if (!createOrUpdateRawValues.length) {
|
||||
logWarning(
|
||||
`An empty "rawValues" array has been passed to the handleRecords method for tableName ${tableName}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ describe('*** Operator: Channel Handlers tests ***', () => {
|
|||
await operator.handleMyChannel({
|
||||
channels,
|
||||
myChannels,
|
||||
prepareRecordsOnly: true,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(spyOnHandleRecords).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -192,7 +192,7 @@ describe('*** Operator: Channel Handlers tests ***', () => {
|
|||
fieldName: 'id',
|
||||
createOrUpdateRawValues: myChannels,
|
||||
tableName: 'MyChannel',
|
||||
prepareRecordsOnly: true,
|
||||
prepareRecordsOnly: false,
|
||||
buildKeyRecordBy: buildMyChannelKey,
|
||||
transformer: transformMyChannelRecord,
|
||||
}, 'handleMyChannel');
|
||||
|
|
@ -253,7 +253,7 @@ describe('*** Operator: Channel Handlers tests ***', () => {
|
|||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(updated[0]).toHaveProperty('lastFetchedAt', 123456789);
|
||||
expect(updated[0].lastFetchedAt).toBe(123456789);
|
||||
});
|
||||
|
||||
it('=> HandleChannelMembership: should write to the CHANNEL_MEMBERSHIP table', async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Model, Q} from '@nozbe/watermelondb';
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {
|
||||
|
|
@ -9,7 +9,6 @@ import {
|
|||
buildChannelMembershipKey,
|
||||
} from '@database/operator/server_data_operator/comparators';
|
||||
import {
|
||||
transformChannelBookmarkRecord,
|
||||
transformChannelInfoRecord,
|
||||
transformChannelMembershipRecord,
|
||||
transformChannelRecord,
|
||||
|
|
@ -18,11 +17,10 @@ import {
|
|||
} from '@database/operator/server_data_operator/transformers/channel';
|
||||
import {getUniqueRawsBy} from '@database/operator/utils/general';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import {logWarning} from '@utils/log';
|
||||
|
||||
import type ServerDataOperatorBase from '.';
|
||||
import type {HandleChannelArgs, HandleChannelBookmarkArgs, HandleChannelInfoArgs, HandleChannelMembershipArgs, HandleMyChannelArgs, HandleMyChannelSettingsArgs} from '@typings/database/database';
|
||||
import type {HandleChannelArgs, HandleChannelInfoArgs, HandleChannelMembershipArgs, HandleMyChannelArgs, HandleMyChannelSettingsArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type ChannelInfoModel from '@typings/database/models/servers/channel_info';
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
|
|
@ -31,7 +29,6 @@ import type MyChannelSettingsModel from '@typings/database/models/servers/my_cha
|
|||
|
||||
const {
|
||||
CHANNEL,
|
||||
CHANNEL_BOOKMARK,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_MEMBERSHIP,
|
||||
MY_CHANNEL,
|
||||
|
|
@ -40,11 +37,10 @@ const {
|
|||
|
||||
export interface ChannelHandlerMix {
|
||||
handleChannel: ({channels, prepareRecordsOnly}: HandleChannelArgs) => Promise<ChannelModel[]>;
|
||||
handleChannelBookmark: ({bookmarks, prepareRecordsOnly}: HandleChannelBookmarkArgs) => Promise<Model[]>;
|
||||
handleChannelMembership: ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => Promise<ChannelMembershipModel[]>;
|
||||
handleMyChannelSettings: ({settings, prepareRecordsOnly}: HandleMyChannelSettingsArgs) => Promise<MyChannelSettingsModel[]>;
|
||||
handleChannelInfo: ({channelInfos, prepareRecordsOnly}: HandleChannelInfoArgs) => Promise<ChannelInfoModel[]>;
|
||||
handleMyChannel: ({channels, myChannels, isCRTEnabled, prepareRecordsOnly}: HandleMyChannelArgs) => Promise<Model[]>;
|
||||
handleMyChannel: ({channels, myChannels, isCRTEnabled, prepareRecordsOnly}: HandleMyChannelArgs) => Promise<MyChannelModel[]>;
|
||||
}
|
||||
|
||||
const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -221,7 +217,7 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
|
|||
* @param {boolean} myChannelsArgs.prepareRecordsOnly
|
||||
* @returns {Promise<MyChannelModel[]>}
|
||||
*/
|
||||
handleMyChannel = async ({channels, myChannels, isCRTEnabled, prepareRecordsOnly = true}: HandleMyChannelArgs): Promise<Model[]> => {
|
||||
handleMyChannel = async ({channels, myChannels, isCRTEnabled, prepareRecordsOnly = true}: HandleMyChannelArgs): Promise<MyChannelModel[]> => {
|
||||
if (!myChannels?.length) {
|
||||
logWarning(
|
||||
'An empty or undefined "myChannels" array has been passed to the handleMyChannel method',
|
||||
|
|
@ -239,6 +235,7 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
|
|||
}
|
||||
|
||||
const isCRT = isCRTEnabled ?? await getIsCRTEnabled(this.database);
|
||||
|
||||
const channelMap = channels.reduce((result: Record<string, Channel>, channel) => {
|
||||
result[channel.id] = channel;
|
||||
return result;
|
||||
|
|
@ -356,87 +353,6 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
|
|||
tableName: CHANNEL_MEMBERSHIP,
|
||||
}, 'handleChannelMembership');
|
||||
};
|
||||
|
||||
/**
|
||||
* handleChannelMembership: Handler responsible for the Create/Update/Delete operations occurring on the CHANNEL_BOOKMARK table from the 'Server' schema
|
||||
* @param {HandleChannelBookmarkArgs} channelBookmarkArgs
|
||||
* @param {ChannelBookmark[]} channelBookmarkArgs.bookmarks
|
||||
* @param {boolean} channelBookmarkArgs.prepareRecordsOnly
|
||||
* @returns {Promise<ChannelBookmarkModel[]>}
|
||||
*/
|
||||
handleChannelBookmark = async ({bookmarks, prepareRecordsOnly}: HandleChannelBookmarkArgs): Promise<Model[]> => {
|
||||
if (!bookmarks?.length) {
|
||||
logWarning(
|
||||
'An empty or undefined "bookmarks" array has been passed to the handleChannelBookmark method',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const uniqueRaws = getUniqueRawsBy({raws: bookmarks, key: 'id'}) as ChannelBookmarkWithFileInfo[];
|
||||
const keys = uniqueRaws.map((c) => c.id);
|
||||
const db: Database = this.database;
|
||||
const existing = await db.get<ChannelBookmarkModel>(CHANNEL_BOOKMARK).query(
|
||||
Q.where('id', Q.oneOf(keys)),
|
||||
).fetch();
|
||||
const bookmarkMap = new Map<string, ChannelBookmarkModel>(existing.map((b) => [b.id, b]));
|
||||
const files: FileInfo[] = [];
|
||||
const raws = uniqueRaws.reduce<{createOrUpdateRaws: ChannelBookmarkWithFileInfo[]; deleteRaws: ChannelBookmarkWithFileInfo[]}>((res, b) => {
|
||||
const e = bookmarkMap.get(b.id);
|
||||
if (!e) {
|
||||
if (!b.delete_at) {
|
||||
res.createOrUpdateRaws.push(b);
|
||||
if (b.file) {
|
||||
files.push(b.file);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
if (e.updateAt !== b.update_at) {
|
||||
res.createOrUpdateRaws.push(b);
|
||||
if (b.file) {
|
||||
files.push(b.file);
|
||||
}
|
||||
}
|
||||
|
||||
if (b.delete_at) {
|
||||
res.deleteRaws.push(b);
|
||||
if (b.file) {
|
||||
b.file.delete_at = b.delete_at;
|
||||
files.push(b.file);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}, {createOrUpdateRaws: [], deleteRaws: []});
|
||||
|
||||
if (!raws.createOrUpdateRaws.length && !raws.deleteRaws.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const preparedBookmarks = await this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformChannelBookmarkRecord,
|
||||
createOrUpdateRawValues: raws.createOrUpdateRaws,
|
||||
deleteRawValues: raws.deleteRaws,
|
||||
tableName: CHANNEL_BOOKMARK,
|
||||
prepareRecordsOnly: true,
|
||||
}, 'handleChannelBookmark');
|
||||
|
||||
const batch: Model[] = [...preparedBookmarks];
|
||||
if (files.length) {
|
||||
const bookmarkFiles = await this.handleFiles({files, prepareRecordsOnly: true});
|
||||
if (bookmarkFiles.length) {
|
||||
batch.push(...bookmarkFiles);
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.length && !prepareRecordsOnly) {
|
||||
await this.batchRecords(batch, 'handleChannelBookmark');
|
||||
}
|
||||
|
||||
return batch;
|
||||
};
|
||||
};
|
||||
|
||||
export default ChannelHandler;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {shouldUpdateFileRecord} from '@database/operator/server_data_operator/comparators/files';
|
||||
import {
|
||||
transformConfigRecord,
|
||||
transformCustomEmojiRecord,
|
||||
|
|
@ -10,7 +9,7 @@ import {
|
|||
transformSystemRecord,
|
||||
} from '@database/operator/server_data_operator/transformers/general';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator/index';
|
||||
import type ServerDataOperator from '..';
|
||||
|
||||
describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||
let operator: ServerDataOperator;
|
||||
|
|
@ -121,59 +120,6 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
}, 'handleConfigs');
|
||||
});
|
||||
|
||||
it('=> HandleFiles: should write to the FILE table', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const spyOnprocessRecords = jest.spyOn(operator, 'processRecords');
|
||||
|
||||
const files = [{
|
||||
id: 'f1oxe5rtepfs7n3zifb4sso7po',
|
||||
user_id: '89ertha8xpfsumpucqppy5knao',
|
||||
post_id: 'a7ebyw883trm884p1qcgt8yw4a',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 0,
|
||||
name: '4qtwrg.jpg',
|
||||
extension: 'jpg',
|
||||
size: 89208,
|
||||
mime_type: 'image/jpeg',
|
||||
width: 500,
|
||||
height: 656,
|
||||
has_preview_image: true,
|
||||
mini_preview:
|
||||
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
|
||||
}, {
|
||||
id: 'f1oxe5rtepfs7n3zifb4sso7po',
|
||||
user_id: 'bookmark',
|
||||
create_at: 1608270920357,
|
||||
update_at: 1608270920357,
|
||||
delete_at: 1608270920357,
|
||||
name: '4qtwrg.jpg',
|
||||
extension: 'jpg',
|
||||
size: 89208,
|
||||
mime_type: 'image/jpeg',
|
||||
width: 500,
|
||||
height: 656,
|
||||
has_preview_image: true,
|
||||
mini_preview:
|
||||
'/9j/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIABAAEAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AN/T/iZp+pX15FpUmnwLbXtpJpyy2sQLw8CcBXA+bksCDnHGOaf4W+P3xIshbQ6loB8RrbK11f3FpbBFW3ZwiFGHB2kr25BIOeCPPbX4S3407T7rTdDfxFNIpDyRaw9lsB4OECHGR15yO4GK6fRPhR4sGmSnxAs8NgchNOjvDPsjz8qSHA37cDk5JPPFdlOpTdPlcVt/Ku1lrvr17b67EPnjrH8/626H/9k=',
|
||||
},
|
||||
];
|
||||
|
||||
await operator.handleFiles({
|
||||
files,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
expect(spyOnprocessRecords).toHaveBeenCalledWith({
|
||||
fieldName: 'id',
|
||||
createOrUpdateRawValues: files.filter((f) => !f.delete_at),
|
||||
deleteRawValues: files.filter((f) => f.delete_at),
|
||||
tableName: 'File',
|
||||
shouldUpdate: shouldUpdateFileRecord,
|
||||
});
|
||||
});
|
||||
|
||||
it('=> No table name: should not call execute if tableName is invalid', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@
|
|||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import BaseDataOperator from '@database/operator/base_data_operator';
|
||||
import {shouldUpdateFileRecord} from '@database/operator/server_data_operator/comparators/files';
|
||||
import {
|
||||
transformConfigRecord,
|
||||
transformCustomEmojiRecord,
|
||||
transformFileRecord,
|
||||
transformRoleRecord,
|
||||
transformSystemRecord,
|
||||
} from '@database/operator/server_data_operator/transformers/general';
|
||||
|
|
@ -18,14 +16,13 @@ import {sanitizeReactions} from '../../utils/reaction';
|
|||
import {transformReactionRecord} from '../transformers/reaction';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type {HandleConfigArgs, HandleCustomEmojiArgs, HandleFilesArgs, HandleReactionsArgs, HandleRoleArgs, HandleSystemArgs, OperationArgs} from '@typings/database/database';
|
||||
import type {HandleConfigArgs, HandleCustomEmojiArgs, HandleReactionsArgs, HandleRoleArgs, HandleSystemArgs, OperationArgs} from '@typings/database/database';
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
import type ReactionModel from '@typings/database/models/servers/reaction';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
const {SERVER: {CONFIG, CUSTOM_EMOJI, FILE, ROLE, SYSTEM, REACTION}} = MM_TABLES;
|
||||
const {SERVER: {CONFIG, CUSTOM_EMOJI, ROLE, SYSTEM, REACTION}} = MM_TABLES;
|
||||
|
||||
export default class ServerDataOperatorBase extends BaseDataOperator {
|
||||
handleRole = async ({roles, prepareRecordsOnly = true}: HandleRoleArgs) => {
|
||||
|
|
@ -154,58 +151,6 @@ export default class ServerDataOperatorBase extends BaseDataOperator {
|
|||
return batchRecords;
|
||||
};
|
||||
|
||||
/**
|
||||
* handleFiles: Handler responsible for the Create/Update operations occurring on the File table from the 'Server' schema
|
||||
* @param {HandleFilesArgs} handleFiles
|
||||
* @param {RawFile[]} handleFiles.files
|
||||
* @param {boolean} handleFiles.prepareRecordsOnly
|
||||
* @returns {Promise<FileModel[]>}
|
||||
*/
|
||||
handleFiles = async ({files, prepareRecordsOnly}: HandleFilesArgs): Promise<FileModel[]> => {
|
||||
if (!files?.length) {
|
||||
logWarning(
|
||||
'An empty or undefined "files" array has been passed to the handleFiles method',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const raws = files.reduce<{createOrUpdateFiles: FileInfo[]; deleteFiles: FileInfo[]}>((res, f) => {
|
||||
if (f.delete_at) {
|
||||
res.deleteFiles.push(f);
|
||||
} else {
|
||||
res.createOrUpdateFiles.push(f);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, {createOrUpdateFiles: [], deleteFiles: []});
|
||||
|
||||
const processedFiles = (await this.processRecords<FileModel>({
|
||||
createOrUpdateRawValues: raws.createOrUpdateFiles,
|
||||
tableName: FILE,
|
||||
fieldName: 'id',
|
||||
deleteRawValues: raws.deleteFiles,
|
||||
shouldUpdate: shouldUpdateFileRecord,
|
||||
}));
|
||||
|
||||
const preparedFiles = await this.prepareRecords<FileModel>({
|
||||
createRaws: processedFiles.createRaws,
|
||||
updateRaws: processedFiles.updateRaws,
|
||||
deleteRaws: processedFiles.deleteRaws,
|
||||
transformer: transformFileRecord,
|
||||
tableName: FILE,
|
||||
});
|
||||
|
||||
if (prepareRecordsOnly) {
|
||||
return preparedFiles;
|
||||
}
|
||||
|
||||
if (preparedFiles?.length) {
|
||||
await this.batchRecords(preparedFiles, 'handleFiles');
|
||||
}
|
||||
|
||||
return preparedFiles;
|
||||
};
|
||||
|
||||
/**
|
||||
* execute: Handles the Create/Update operations on an table.
|
||||
* @param {OperationArgs} execute
|
||||
|
|
|
|||
|
|
@ -6,15 +6,16 @@ import {Q} from '@nozbe/watermelondb';
|
|||
import {ActionType} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {buildDraftKey} from '@database/operator/server_data_operator/comparators';
|
||||
import {shouldUpdateFileRecord} from '@database/operator/server_data_operator/comparators/files';
|
||||
import {
|
||||
transformDraftRecord,
|
||||
transformFileRecord,
|
||||
transformPostInThreadRecord,
|
||||
transformPostRecord,
|
||||
transformPostsInChannelRecord,
|
||||
} from '@database/operator/server_data_operator/transformers/post';
|
||||
import {getRawRecordPairs, getUniqueRawsBy, getValidRecordsForUpdate} from '@database/operator/utils/general';
|
||||
import {createPostsChain, getPostListEdges} from '@database/operator/utils/post';
|
||||
import FileModel from '@typings/database/models/servers/file';
|
||||
import {logWarning} from '@utils/log';
|
||||
|
||||
import type ServerDataOperatorBase from '.';
|
||||
|
|
@ -22,6 +23,7 @@ import type Database from '@nozbe/watermelondb/Database';
|
|||
import type Model from '@nozbe/watermelondb/Model';
|
||||
import type {HandleDraftArgs, HandleFilesArgs, HandlePostsArgs, RecordPair} from '@typings/database/database';
|
||||
import type DraftModel from '@typings/database/models/servers/draft';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type PostsInChannelModel from '@typings/database/models/servers/posts_in_channel';
|
||||
import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread';
|
||||
|
|
@ -29,6 +31,7 @@ import type ReactionModel from '@typings/database/models/servers/reaction';
|
|||
|
||||
const {
|
||||
DRAFT,
|
||||
FILE,
|
||||
POST,
|
||||
POSTS_IN_CHANNEL,
|
||||
POSTS_IN_THREAD,
|
||||
|
|
@ -288,6 +291,47 @@ const PostHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supercla
|
|||
return batch;
|
||||
};
|
||||
|
||||
/**
|
||||
* handleFiles: Handler responsible for the Create/Update operations occurring on the File table from the 'Server' schema
|
||||
* @param {HandleFilesArgs} handleFiles
|
||||
* @param {RawFile[]} handleFiles.files
|
||||
* @param {boolean} handleFiles.prepareRecordsOnly
|
||||
* @returns {Promise<FileModel[]>}
|
||||
*/
|
||||
handleFiles = async ({files, prepareRecordsOnly}: HandleFilesArgs): Promise<FileModel[]> => {
|
||||
if (!files?.length) {
|
||||
logWarning(
|
||||
'An empty or undefined "files" array has been passed to the handleFiles method',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const processedFiles = (await this.processRecords({
|
||||
createOrUpdateRawValues: files,
|
||||
tableName: FILE,
|
||||
fieldName: 'id',
|
||||
deleteRawValues: [],
|
||||
shouldUpdate: shouldUpdateFileRecord,
|
||||
}));
|
||||
|
||||
const postFiles = await this.prepareRecords({
|
||||
createRaws: processedFiles.createRaws,
|
||||
updateRaws: processedFiles.updateRaws,
|
||||
transformer: transformFileRecord,
|
||||
tableName: FILE,
|
||||
});
|
||||
|
||||
if (prepareRecordsOnly) {
|
||||
return postFiles;
|
||||
}
|
||||
|
||||
if (postFiles?.length) {
|
||||
await this.batchRecords(postFiles, 'handleFiles');
|
||||
}
|
||||
|
||||
return postFiles;
|
||||
};
|
||||
|
||||
/**
|
||||
* handlePostsInThread: Handler responsible for the Create/Update operations occurring on the PostsInThread table from the 'Server' schema
|
||||
* @param {Record<string, Post[]>} postsMap
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {extractChannelDisplayName} from '@helpers/database';
|
|||
|
||||
import type {TransformerArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import type ChannelInfoModel from '@typings/database/models/servers/channel_info';
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
|
|
@ -15,7 +14,6 @@ import type MyChannelSettingsModel from '@typings/database/models/servers/my_cha
|
|||
|
||||
const {
|
||||
CHANNEL,
|
||||
CHANNEL_BOOKMARK,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_MEMBERSHIP,
|
||||
MY_CHANNEL,
|
||||
|
|
@ -180,44 +178,3 @@ export const transformChannelMembershipRecord = ({action, database, value}: Tran
|
|||
fieldsMapper,
|
||||
}) as Promise<ChannelMembershipModel>;
|
||||
};
|
||||
|
||||
/**
|
||||
* transformChannelBookmarkRecord: Prepares a record of the SERVER database 'Channel' table for update or create actions.
|
||||
* @param {DataFactory} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordPair} operator.value
|
||||
* @returns {Promise<ChannelBookmarkModel>}
|
||||
*/
|
||||
export const transformChannelBookmarkRecord = ({action, database, value}: TransformerArgs): Promise<ChannelBookmarkModel> => {
|
||||
const raw = value.raw as ChannelBookmark;
|
||||
const record = value.record as ChannelBookmarkModel;
|
||||
const isCreateAction = action === OperationType.CREATE;
|
||||
|
||||
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
|
||||
const fieldsMapper = (bookmark: ChannelBookmarkModel) => {
|
||||
bookmark._raw.id = isCreateAction ? (raw?.id ?? bookmark.id) : record.id;
|
||||
bookmark.createAt = raw.create_at;
|
||||
bookmark.deleteAt = raw.delete_at;
|
||||
bookmark.updateAt = raw.update_at;
|
||||
bookmark.channelId = raw.channel_id;
|
||||
bookmark.ownerId = raw.owner_id;
|
||||
bookmark.fileId = raw.file_id;
|
||||
|
||||
bookmark.displayName = raw.display_name;
|
||||
bookmark.sortOrder = raw.sort_order;
|
||||
bookmark.linkUrl = raw.link_url;
|
||||
bookmark.imageUrl = raw.image_url;
|
||||
bookmark.emoji = raw.emoji;
|
||||
bookmark.type = raw.type;
|
||||
bookmark.originalId = raw.original_id;
|
||||
bookmark.parentId = raw.parent_id;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
action,
|
||||
database,
|
||||
tableName: CHANNEL_BOOKMARK,
|
||||
value,
|
||||
fieldsMapper,
|
||||
}) as Promise<ChannelBookmarkModel>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import {OperationType} from '@constants/database';
|
||||
import {
|
||||
transformCustomEmojiRecord,
|
||||
transformFileRecord,
|
||||
transformRoleRecord,
|
||||
transformSystemRecord,
|
||||
} from '@database/operator/server_data_operator/transformers/general';
|
||||
|
|
@ -84,37 +83,3 @@ describe('*** CustomEmoj Prepare Records Test ***', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('*** Files Prepare Records Test ***', () => {
|
||||
it('=> transformFileRecord: should return an array of type File', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await transformFileRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'file-id',
|
||||
post_id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
name: 'test_file',
|
||||
extension: '.jpg',
|
||||
has_preview_image: true,
|
||||
mime_type: 'image/jpeg',
|
||||
size: 1000,
|
||||
create_at: 1609253011321,
|
||||
delete_at: 1609253011321,
|
||||
height: 20,
|
||||
width: 20,
|
||||
update_at: 1609253011321,
|
||||
user_id: 'wqyby5r5pinxxdqhoaomtacdhc',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.table).toBe('File');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,13 +7,11 @@ import {prepareBaseRecord} from '@database/operator/server_data_operator/transfo
|
|||
import type {TransformerArgs} from '@typings/database/database';
|
||||
import type ConfigModel from '@typings/database/models/servers/config';
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
import type RoleModel from '@typings/database/models/servers/role';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
const {
|
||||
CUSTOM_EMOJI,
|
||||
FILE,
|
||||
ROLE,
|
||||
SYSTEM,
|
||||
CONFIG,
|
||||
|
|
@ -123,38 +121,3 @@ export const transformConfigRecord = ({action, database, value}: TransformerArgs
|
|||
fieldsMapper,
|
||||
}) as Promise<ConfigModel>;
|
||||
};
|
||||
|
||||
/**
|
||||
* transformFileRecord: Prepares a record of the SERVER database 'Files' table for update or create actions.
|
||||
* @param {TransformerArgs} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordPair} operator.value
|
||||
* @returns {Promise<FileModel>}
|
||||
*/
|
||||
export const transformFileRecord = ({action, database, value}: TransformerArgs): Promise<FileModel> => {
|
||||
const raw = value.raw as FileInfo;
|
||||
const record = value.record as FileModel;
|
||||
const isCreateAction = action === OperationType.CREATE;
|
||||
|
||||
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
|
||||
const fieldsMapper = (file: FileModel) => {
|
||||
file._raw.id = isCreateAction ? (raw.id || file.id) : record.id;
|
||||
file.postId = raw.post_id || '';
|
||||
file.name = raw.name;
|
||||
file.extension = raw.extension;
|
||||
file.size = raw.size;
|
||||
file.mimeType = raw?.mime_type ?? '';
|
||||
file.width = raw?.width || record?.width || 0;
|
||||
file.height = raw?.height || record?.height || 0;
|
||||
file.imageThumbnail = raw?.mini_preview || record?.imageThumbnail || '';
|
||||
file.localPath = raw?.localPath || record?.localPath || null;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
action,
|
||||
database,
|
||||
tableName: FILE,
|
||||
value,
|
||||
fieldsMapper,
|
||||
}) as Promise<FileModel>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import {OperationType} from '@constants/database';
|
||||
import {
|
||||
transformDraftRecord,
|
||||
transformFileRecord,
|
||||
transformPostInThreadRecord,
|
||||
transformPostRecord,
|
||||
transformPostsInChannelRecord,
|
||||
|
|
@ -74,6 +75,39 @@ describe('*** POST Prepare Records Test ***', () => {
|
|||
expect(preparedRecords!.collection.table).toBe('PostsInThread');
|
||||
});
|
||||
|
||||
it('=> transformFileRecord: should return an array of type File', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await createTestConnection({databaseName: 'post_prepare_records', setActive: true});
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await transformFileRecord({
|
||||
action: OperationType.CREATE,
|
||||
database: database!,
|
||||
value: {
|
||||
record: undefined,
|
||||
raw: {
|
||||
id: 'file-id',
|
||||
post_id: 'ps81iqbddesfby8jayz7owg4yypoo',
|
||||
name: 'test_file',
|
||||
extension: '.jpg',
|
||||
has_preview_image: true,
|
||||
mime_type: 'image/jpeg',
|
||||
size: 1000,
|
||||
create_at: 1609253011321,
|
||||
delete_at: 1609253011321,
|
||||
height: 20,
|
||||
width: 20,
|
||||
update_at: 1609253011321,
|
||||
user_id: 'wqyby5r5pinxxdqhoaomtacdhc',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(preparedRecords).toBeTruthy();
|
||||
expect(preparedRecords!.collection.table).toBe('File');
|
||||
});
|
||||
|
||||
it('=> transformDraftRecord: should return an array of type Draft', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import {prepareBaseRecord} from '@database/operator/server_data_operator/transfo
|
|||
|
||||
import type{TransformerArgs} from '@typings/database/database';
|
||||
import type DraftModel from '@typings/database/models/servers/draft';
|
||||
import type FileModel from '@typings/database/models/servers/file';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type PostsInChannelModel from '@typings/database/models/servers/posts_in_channel';
|
||||
import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread';
|
||||
|
||||
const {
|
||||
DRAFT,
|
||||
FILE,
|
||||
POST,
|
||||
POSTS_IN_CHANNEL,
|
||||
POSTS_IN_THREAD,
|
||||
|
|
@ -92,6 +94,41 @@ export const transformPostInThreadRecord = ({action, database, value}: Transform
|
|||
}) as Promise<PostsInThreadModel>;
|
||||
};
|
||||
|
||||
/**
|
||||
* transformFileRecord: Prepares a record of the SERVER database 'Files' table for update or create actions.
|
||||
* @param {TransformerArgs} operator
|
||||
* @param {Database} operator.database
|
||||
* @param {RecordPair} operator.value
|
||||
* @returns {Promise<FileModel>}
|
||||
*/
|
||||
export const transformFileRecord = ({action, database, value}: TransformerArgs): Promise<FileModel> => {
|
||||
const raw = value.raw as FileInfo;
|
||||
const record = value.record as FileModel;
|
||||
const isCreateAction = action === OperationType.CREATE;
|
||||
|
||||
// If isCreateAction is true, we will use the id (API response) from the RAW, else we shall use the existing record id from the database
|
||||
const fieldsMapper = (file: FileModel) => {
|
||||
file._raw.id = isCreateAction ? (raw.id || file.id) : record.id;
|
||||
file.postId = raw.post_id;
|
||||
file.name = raw.name;
|
||||
file.extension = raw.extension;
|
||||
file.size = raw.size;
|
||||
file.mimeType = raw?.mime_type ?? '';
|
||||
file.width = raw?.width || record?.width || 0;
|
||||
file.height = raw?.height || record?.height || 0;
|
||||
file.imageThumbnail = raw?.mini_preview || record?.imageThumbnail || '';
|
||||
file.localPath = raw?.localPath || record?.localPath || null;
|
||||
};
|
||||
|
||||
return prepareBaseRecord({
|
||||
action,
|
||||
database,
|
||||
tableName: FILE,
|
||||
value,
|
||||
fieldsMapper,
|
||||
}) as Promise<FileModel>;
|
||||
};
|
||||
|
||||
/**
|
||||
* transformDraftRecord: Prepares a record of the SERVER database 'Draft' table for update or create actions.
|
||||
* @param {TransformerArgs} operator
|
||||
|
|
|
|||
|
|
@ -6,10 +6,9 @@ import {type AppSchema, appSchema} from '@nozbe/watermelondb';
|
|||
import {
|
||||
CategorySchema,
|
||||
CategoryChannelSchema,
|
||||
ChannelSchema,
|
||||
ChannelBookmarkSchema,
|
||||
ChannelInfoSchema,
|
||||
ChannelMembershipSchema,
|
||||
ChannelSchema,
|
||||
ConfigSchema,
|
||||
CustomEmojiSchema,
|
||||
DraftSchema,
|
||||
|
|
@ -40,14 +39,13 @@ import {
|
|||
} from './table_schemas';
|
||||
|
||||
export const serverSchema: AppSchema = appSchema({
|
||||
version: 4,
|
||||
version: 3,
|
||||
tables: [
|
||||
CategorySchema,
|
||||
CategoryChannelSchema,
|
||||
ChannelSchema,
|
||||
ChannelBookmarkSchema,
|
||||
ChannelInfoSchema,
|
||||
ChannelMembershipSchema,
|
||||
ChannelSchema,
|
||||
ConfigSchema,
|
||||
CustomEmojiSchema,
|
||||
DraftSchema,
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {tableSchema} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
const {CHANNEL_BOOKMARK} = MM_TABLES.SERVER;
|
||||
|
||||
export default tableSchema({
|
||||
name: CHANNEL_BOOKMARK,
|
||||
columns: [
|
||||
{name: 'create_at', type: 'number'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'delete_at', type: 'number'},
|
||||
{name: 'channel_id', type: 'string', isIndexed: true},
|
||||
{name: 'owner_id', type: 'string'},
|
||||
{name: 'file_id', type: 'string', isOptional: true},
|
||||
{name: 'display_name', type: 'string'},
|
||||
{name: 'sort_order', type: 'number'},
|
||||
{name: 'link_url', type: 'string', isOptional: true},
|
||||
{name: 'image_url', type: 'string', isOptional: true},
|
||||
{name: 'emoji', type: 'string', isOptional: true},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'original_id', type: 'string', isOptional: true},
|
||||
{name: 'parent_id', type: 'string', isOptional: true},
|
||||
],
|
||||
});
|
||||
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
export {default as CategorySchema} from './category';
|
||||
export {default as CategoryChannelSchema} from './category_channel';
|
||||
export {default as ChannelSchema} from './channel';
|
||||
export {default as ChannelBookmarkSchema} from './channel_bookmark';
|
||||
export {default as ChannelInfoSchema} from './channel_info';
|
||||
export {default as ChannelMembershipSchema} from './channel_membership';
|
||||
export {default as ChannelSchema} from './channel';
|
||||
export {default as CustomEmojiSchema} from './custom_emoji';
|
||||
export {default as DraftSchema} from './draft';
|
||||
export {default as FileSchema} from './file';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ const {
|
|||
CATEGORY,
|
||||
CATEGORY_CHANNEL,
|
||||
CHANNEL,
|
||||
CHANNEL_BOOKMARK,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_MEMBERSHIP,
|
||||
CONFIG,
|
||||
|
|
@ -46,7 +45,7 @@ const {
|
|||
describe('*** Test schema for SERVER database ***', () => {
|
||||
it('=> The SERVER SCHEMA should strictly match', () => {
|
||||
expect(serverSchema).toEqual({
|
||||
version: 4,
|
||||
version: 3,
|
||||
unsafeSql: undefined,
|
||||
tables: {
|
||||
[CATEGORY]: {
|
||||
|
|
@ -137,43 +136,6 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
{name: 'update_at', type: 'number'},
|
||||
],
|
||||
},
|
||||
[CHANNEL_BOOKMARK]: {
|
||||
name: CHANNEL_BOOKMARK,
|
||||
unsafeSql: undefined,
|
||||
columns: {
|
||||
create_at: {name: 'create_at', type: 'number'},
|
||||
update_at: {name: 'update_at', type: 'number'},
|
||||
delete_at: {name: 'delete_at', type: 'number'},
|
||||
channel_id: {name: 'channel_id', type: 'string', isIndexed: true},
|
||||
owner_id: {name: 'owner_id', type: 'string'},
|
||||
file_id: {name: 'file_id', type: 'string', isOptional: true},
|
||||
display_name: {name: 'display_name', type: 'string'},
|
||||
sort_order: {name: 'sort_order', type: 'number'},
|
||||
link_url: {name: 'link_url', type: 'string', isOptional: true},
|
||||
image_url: {name: 'image_url', type: 'string', isOptional: true},
|
||||
emoji: {name: 'emoji', type: 'string', isOptional: true},
|
||||
type: {name: 'type', type: 'string'},
|
||||
original_id: {name: 'original_id', type: 'string', isOptional: true},
|
||||
parent_id: {name: 'parent_id', type: 'string', isOptional: true},
|
||||
|
||||
},
|
||||
columnArray: [
|
||||
{name: 'create_at', type: 'number'},
|
||||
{name: 'update_at', type: 'number'},
|
||||
{name: 'delete_at', type: 'number'},
|
||||
{name: 'channel_id', type: 'string', isIndexed: true},
|
||||
{name: 'owner_id', type: 'string'},
|
||||
{name: 'file_id', type: 'string', isOptional: true},
|
||||
{name: 'display_name', type: 'string'},
|
||||
{name: 'sort_order', type: 'number'},
|
||||
{name: 'link_url', type: 'string', isOptional: true},
|
||||
{name: 'image_url', type: 'string', isOptional: true},
|
||||
{name: 'emoji', type: 'string', isOptional: true},
|
||||
{name: 'type', type: 'string'},
|
||||
{name: 'original_id', type: 'string', isOptional: true},
|
||||
{name: 'parent_id', type: 'string', isOptional: true},
|
||||
],
|
||||
},
|
||||
[CHANNEL_MEMBERSHIP]: {
|
||||
name: CHANNEL_MEMBERSHIP,
|
||||
unsafeSql: undefined,
|
||||
|
|
|
|||
|
|
@ -1,53 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {useMemo} from 'react';
|
||||
|
||||
import {getLocalFileInfo} from '@actions/local/file';
|
||||
import {buildFilePreviewUrl, buildFileUrl} from '@actions/remote/file';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {isGif, isImage, isVideo} from '@utils/file';
|
||||
import {getImageSize} from '@utils/gallery';
|
||||
|
||||
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
|
||||
const getFileInfo = async (serverUrl: string, bookmarks: ChannelBookmarkModel[], publicLinkEnabled: boolean, cb: (files: FileInfo[]) => void) => {
|
||||
const fileInfos: FileInfo[] = [];
|
||||
for await (const b of bookmarks) {
|
||||
if (b.fileId) {
|
||||
const res = await getLocalFileInfo(serverUrl, b.fileId);
|
||||
if (res.file) {
|
||||
const fileInfo = res.file.toFileInfo(b.ownerId);
|
||||
const imageFile = isImage(fileInfo);
|
||||
const videoFile = isVideo(fileInfo);
|
||||
|
||||
let uri;
|
||||
if (imageFile || (videoFile && publicLinkEnabled)) {
|
||||
if (fileInfo.localPath) {
|
||||
uri = fileInfo.localPath;
|
||||
} else {
|
||||
uri = (isGif(fileInfo) || (imageFile && !fileInfo.has_preview_image) || videoFile) ? buildFileUrl(serverUrl, fileInfo.id!) : buildFilePreviewUrl(serverUrl, fileInfo.id!);
|
||||
}
|
||||
} else {
|
||||
uri = fileInfo.localPath || buildFileUrl(serverUrl, fileInfo.id!);
|
||||
}
|
||||
|
||||
let {width, height} = fileInfo;
|
||||
if (imageFile && !width) {
|
||||
const size = await getImageSize(uri);
|
||||
width = size.width;
|
||||
height = size.height;
|
||||
}
|
||||
|
||||
fileInfos.push({...fileInfo, uri, width, height});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileInfos.length) {
|
||||
cb(fileInfos);
|
||||
}
|
||||
};
|
||||
|
||||
export const useImageAttachments = (filesInfo: FileInfo[], publicLinkEnabled: boolean) => {
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -77,13 +35,3 @@ export const useImageAttachments = (filesInfo: FileInfo[], publicLinkEnabled: bo
|
|||
}, [filesInfo, publicLinkEnabled]);
|
||||
};
|
||||
|
||||
export const useChannelBookmarkFiles = (bookmarks: ChannelBookmarkModel[], publicLinkEnabled: boolean) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const [files, setFiles] = useState<FileInfo[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getFileInfo(serverUrl, bookmarks, publicLinkEnabled, setFiles);
|
||||
}, [serverUrl, bookmarks, publicLinkEnabled]);
|
||||
|
||||
return files;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,11 +8,9 @@ import BackgroundTimer from 'react-native-background-timer';
|
|||
import {BehaviorSubject} from 'rxjs';
|
||||
import {distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {setAppInactiveSince} from '@actions/app/global';
|
||||
import {setCurrentUserStatus} from '@actions/local/user';
|
||||
import {fetchStatusByIds} from '@actions/remote/user';
|
||||
import {handleClose, handleFirstConnect, handleReconnect} from '@actions/websocket';
|
||||
import {handleWebSocketEvent} from '@actions/websocket/event';
|
||||
import {handleClose, handleEvent, handleFirstConnect, handleReconnect} from '@actions/websocket';
|
||||
import WebSocketClient from '@client/websocket';
|
||||
import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
|
@ -55,9 +53,6 @@ class WebsocketManager {
|
|||
);
|
||||
|
||||
AppState.addEventListener('change', this.onAppStateChange);
|
||||
AppState.addEventListener('blur', () => {
|
||||
setAppInactiveSince(Date.now());
|
||||
});
|
||||
NetInfo.addEventListener(this.onNetStateChange);
|
||||
};
|
||||
|
||||
|
|
@ -82,7 +77,7 @@ class WebsocketManager {
|
|||
const client = new WebSocketClient(serverUrl, bearerToken, storedLastDisconnect);
|
||||
|
||||
client.setFirstConnectCallback(() => this.onFirstConnect(serverUrl));
|
||||
client.setEventCallback((evt: WebSocketMessage) => handleWebSocketEvent(serverUrl, evt));
|
||||
client.setEventCallback((evt: any) => handleEvent(serverUrl, evt));
|
||||
|
||||
//client.setMissedEventsCallback(() => {}) Nothing to do on missedEvents callback
|
||||
client.setReconnectCallback(() => this.onReconnect(serverUrl));
|
||||
|
|
@ -235,7 +230,6 @@ class WebsocketManager {
|
|||
|
||||
this.cancelAllConnections();
|
||||
if (!isActive && !this.isBackgroundTimerRunning) {
|
||||
setAppInactiveSince(Date.now());
|
||||
this.isBackgroundTimerRunning = true;
|
||||
this.cancelAllConnections();
|
||||
this.backgroundIntervalId = BackgroundTimer.setInterval(() => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
|||
import CurrentCallBar from '@calls/components/current_call_bar';
|
||||
import {IncomingCallsContainer} from '@calls/components/incoming_calls_container';
|
||||
import JoinCallBanner from '@calls/components/join_call_banner';
|
||||
import {BOOKMARKS_BAR_HEIGHT, DEFAULT_HEADER_HEIGHT, TABLET_HEADER_HEIGHT} from '@constants/view';
|
||||
import {DEFAULT_HEADER_HEIGHT, TABLET_HEADER_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
|
||||
|
|
@ -28,7 +28,6 @@ type Props = {
|
|||
isInACall?: boolean;
|
||||
threadScreen?: boolean;
|
||||
channelsScreen?: boolean;
|
||||
includeBookmarkBar?: boolean;
|
||||
}
|
||||
|
||||
const FloatingCallContainer = ({
|
||||
|
|
@ -38,7 +37,6 @@ const FloatingCallContainer = ({
|
|||
isInACall,
|
||||
threadScreen,
|
||||
channelsScreen,
|
||||
includeBookmarkBar,
|
||||
}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
|
@ -47,7 +45,7 @@ const FloatingCallContainer = ({
|
|||
const topBarForTablet = (isTablet && !threadScreen) ? TABLET_HEADER_HEIGHT : 0;
|
||||
const topBarChannel = (!isTablet && !threadScreen) ? DEFAULT_HEADER_HEIGHT : 0;
|
||||
const wrapperTop = {
|
||||
top: insets.top + topBarForTablet + topBarChannel + (includeBookmarkBar ? BOOKMARKS_BAR_HEIGHT : 0),
|
||||
top: insets.top + topBarForTablet + topBarChannel,
|
||||
};
|
||||
const wrapperBottom = {
|
||||
bottom: 8,
|
||||
|
|
|
|||
|
|
@ -106,16 +106,14 @@ const micPermission = Platform.select({
|
|||
|
||||
export const usePermissionsChecker = (micPermissionsGranted: boolean) => {
|
||||
const appState = useAppState();
|
||||
const [hasPermission, setHasPermission] = useState(micPermissionsGranted);
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFn = async () => {
|
||||
if (appState === 'active') {
|
||||
const result = (await Permissions.check(micPermission)) === Permissions.RESULTS.GRANTED;
|
||||
setHasPermission(result);
|
||||
if (result) {
|
||||
const hasPermission = (await Permissions.check(micPermission)) === Permissions.RESULTS.GRANTED;
|
||||
if (hasPermission) {
|
||||
initializeVoiceTrack();
|
||||
setMicPermissionsGranted(result);
|
||||
setMicPermissionsGranted(hasPermission);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -123,8 +121,6 @@ export const usePermissionsChecker = (micPermissionsGranted: boolean) => {
|
|||
asyncFn();
|
||||
}
|
||||
}, [appState]);
|
||||
|
||||
return hasPermission;
|
||||
};
|
||||
|
||||
export const useCallsAdjustment = (serverUrl: string, channelId: string): number => {
|
||||
|
|
@ -134,7 +130,6 @@ export const useCallsAdjustment = (serverUrl: string, channelId: string): number
|
|||
const globalCallsState = useGlobalCallsState();
|
||||
const currentCall = useCurrentCall();
|
||||
const [numServers, setNumServers] = useState(1);
|
||||
const micPermissionsGranted = usePermissionsChecker(globalCallsState.micPermissionsGranted);
|
||||
const dismissed = Boolean(callsState.calls[channelId]?.dismissed[callsState.myUserId]);
|
||||
const inCurrentCall = currentCall?.id === channelId;
|
||||
const joinCallBannerVisible = Boolean(channelsWithCalls[channelId]) && !dismissed && !inCurrentCall;
|
||||
|
|
@ -149,7 +144,7 @@ export const useCallsAdjustment = (serverUrl: string, channelId: string): number
|
|||
|
||||
// Do we have calls banners?
|
||||
const currentCallBarVisible = Boolean(currentCall);
|
||||
const micPermissionsError = !micPermissionsGranted && (currentCall && !currentCall.micPermissionsErrorDismissed);
|
||||
const micPermissionsError = !globalCallsState.micPermissionsGranted && (currentCall && !currentCall.micPermissionsErrorDismissed);
|
||||
const callQualityAlert = Boolean(currentCall?.callQualityAlert);
|
||||
const incomingCallsShowing = incomingCalls.filter((ic) => ic.channelID !== channelId);
|
||||
const notificationBarHeight = CALL_NOTIFICATION_BAR_HEIGHT + (numServers > 1 ? 8 : 0);
|
||||
|
|
|
|||
|
|
@ -100,12 +100,3 @@ export const observeTutorialWatched = (tutorial: string) => {
|
|||
switchMap((v) => of$(Boolean(v))),
|
||||
);
|
||||
};
|
||||
|
||||
export const getAppInactiveSince = async () => {
|
||||
try {
|
||||
const records = await queryGlobalValue(GLOBAL_IDENTIFIERS.APP_INACTIVE_SINCE)?.fetch();
|
||||
return (parseInt(records?.[0]?.value || 0, 10) || 0);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import {observeTeammateNameDisplay} from './user';
|
|||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Clause} from '@nozbe/watermelondb/QueryDescription';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import type ChannelInfoModel from '@typings/database/models/servers/channel_info';
|
||||
import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
|
|
@ -29,29 +28,6 @@ import type UserModel from '@typings/database/models/servers/user';
|
|||
|
||||
const {SERVER: {CHANNEL, MY_CHANNEL, CHANNEL_MEMBERSHIP, MY_CHANNEL_SETTINGS, CHANNEL_INFO, USER, TEAM}} = MM_TABLES;
|
||||
|
||||
type ChannelMembershipsExtended = Pick<ChannelMembership, 'user_id' | 'channel_id' | 'scheme_admin'>;
|
||||
|
||||
function prepareChannels(
|
||||
operator: ServerDataOperator,
|
||||
channels?: Channel[],
|
||||
channelInfos?: ChannelInfo[],
|
||||
channelMemberships?: ChannelMembershipsExtended[],
|
||||
memberships?: ChannelMembership[],
|
||||
isCRTEnabled?: boolean,
|
||||
): Array<Promise<Model[]>> {
|
||||
try {
|
||||
const channelRecords = operator.handleChannel({channels, prepareRecordsOnly: true});
|
||||
const channelInfoRecords = operator.handleChannelInfo({channelInfos, prepareRecordsOnly: true});
|
||||
const membershipRecords = operator.handleChannelMembership({channelMemberships, prepareRecordsOnly: true});
|
||||
const myChannelRecords = operator.handleMyChannel({channels, myChannels: memberships, prepareRecordsOnly: true, isCRTEnabled});
|
||||
const myChannelSettingsRecords = operator.handleMyChannelSettings({settings: memberships, prepareRecordsOnly: true});
|
||||
|
||||
return [channelRecords, channelInfoRecords, membershipRecords, myChannelRecords, myChannelSettingsRecords];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareMissingChannelsForAllTeams(operator: ServerDataOperator, channels: Channel[], channelMembers: ChannelMembership[], isCRTEnabled?: boolean): Array<Promise<Model[]>> {
|
||||
const channelInfos: ChannelInfo[] = [];
|
||||
const channelMap: Record<string, Channel> = {};
|
||||
|
|
@ -78,7 +54,17 @@ export function prepareMissingChannelsForAllTeams(operator: ServerDataOperator,
|
|||
};
|
||||
});
|
||||
|
||||
return prepareChannels(operator, channels, channelInfos, memberships, memberships, isCRTEnabled);
|
||||
try {
|
||||
const channelRecords = operator.handleChannel({channels, prepareRecordsOnly: true});
|
||||
const channelInfoRecords = operator.handleChannelInfo({channelInfos, prepareRecordsOnly: true});
|
||||
const membershipRecords = operator.handleChannelMembership({channelMemberships: memberships, prepareRecordsOnly: true});
|
||||
const myChannelRecords = operator.handleMyChannel({channels, myChannels: memberships, prepareRecordsOnly: true, isCRTEnabled});
|
||||
const myChannelSettingsRecords = operator.handleMyChannelSettings({settings: memberships, prepareRecordsOnly: true});
|
||||
|
||||
return [channelRecords, channelInfoRecords, membershipRecords, myChannelRecords, myChannelSettingsRecords];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[], isCRTEnabled?: boolean) => {
|
||||
|
|
@ -131,7 +117,17 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea
|
|||
});
|
||||
}
|
||||
|
||||
return prepareChannels(operator, channels, channelInfos, channelMembers, memberships, isCRTEnabled);
|
||||
try {
|
||||
const channelRecords = operator.handleChannel({channels, prepareRecordsOnly: true});
|
||||
const channelInfoRecords = operator.handleChannelInfo({channelInfos, prepareRecordsOnly: true});
|
||||
const membershipRecords = operator.handleChannelMembership({channelMemberships: channelMembers, prepareRecordsOnly: true});
|
||||
const myChannelRecords = operator.handleMyChannel({channels, myChannels: memberships, prepareRecordsOnly: true, isCRTEnabled});
|
||||
const myChannelSettingsRecords = operator.handleMyChannelSettings({settings: memberships, prepareRecordsOnly: true});
|
||||
|
||||
return [channelRecords, channelInfoRecords, membershipRecords, myChannelRecords, myChannelSettingsRecords];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const prepareDeleteChannel = async (channel: ChannelModel): Promise<Model[]> => {
|
||||
|
|
@ -173,27 +169,6 @@ export const prepareDeleteChannel = async (channel: ChannelModel): Promise<Model
|
|||
}
|
||||
}
|
||||
|
||||
const bookmarks = await channel.bookmarks?.fetch();
|
||||
if (bookmarks?.length) {
|
||||
for await (const bookmark of bookmarks) {
|
||||
const prepareBookmarks = await prepareDeleteBookmarks(bookmark);
|
||||
preparedModels.push(...prepareBookmarks);
|
||||
}
|
||||
}
|
||||
|
||||
return preparedModels;
|
||||
};
|
||||
|
||||
export const prepareDeleteBookmarks = async (bookmark: ChannelBookmarkModel) => {
|
||||
const preparedModels: Model[] = [bookmark.prepareDestroyPermanently()];
|
||||
try {
|
||||
if (bookmark.fileId) {
|
||||
const file = await bookmark.file.fetch();
|
||||
preparedModels.push(file.prepareDestroyPermanently());
|
||||
}
|
||||
} catch {
|
||||
// Record not found, do nothing
|
||||
}
|
||||
return preparedModels;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q, type Database} from '@nozbe/watermelondb';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap, combineLatestWith} from 'rxjs/operators';
|
||||
|
||||
import {General, Permissions} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
|
||||
import {observeChannel} from './channel';
|
||||
import {observePermissionForChannel} from './role';
|
||||
import {observeConfigValue} from './system';
|
||||
import {observeCurrentUser} from './user';
|
||||
|
||||
const {CHANNEL_BOOKMARK} = MM_TABLES.SERVER;
|
||||
|
||||
const observeHasPermissionToBookmarks = (
|
||||
database: Database,
|
||||
channelId: string,
|
||||
public_permission: string,
|
||||
private_permission: string,
|
||||
) => {
|
||||
const serverVersion = observeConfigValue(database, 'Version');
|
||||
const currentUser = observeCurrentUser(database);
|
||||
|
||||
return observeChannel(database, channelId).pipe(
|
||||
combineLatestWith(currentUser, serverVersion),
|
||||
switchMap(([c, user, version]) => {
|
||||
if (!c || !user || c.deleteAt !== 0 || user?.isGuest || !isMinimumServerVersion(version || '', 9, 4)) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
if (isDMorGM(c)) {
|
||||
return of$(true);
|
||||
}
|
||||
|
||||
const permission = c.type === General.OPEN_CHANNEL ? public_permission : private_permission;
|
||||
return observePermissionForChannel(database, c, user, permission, true);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
export const observeCanAddBookmarks = (database: Database, channelId: string) => {
|
||||
return observeHasPermissionToBookmarks(
|
||||
database,
|
||||
channelId,
|
||||
Permissions.ADD_BOOKMARK_PUBLIC_CHANNEL,
|
||||
Permissions.ADD_BOOKMARK_PRIVATE_CHANNEL,
|
||||
);
|
||||
};
|
||||
|
||||
export const observeCanEditBookmarks = (database: Database, channelId: string) => {
|
||||
return observeHasPermissionToBookmarks(
|
||||
database,
|
||||
channelId,
|
||||
Permissions.EDIT_BOOKMARK_PUBLIC_CHANNEL,
|
||||
Permissions.EDIT_BOOKMARK_PRIVATE_CHANNEL,
|
||||
);
|
||||
};
|
||||
|
||||
export const observeCanDeleteBookmarks = (database: Database, channelId: string) => {
|
||||
return observeHasPermissionToBookmarks(
|
||||
database,
|
||||
channelId,
|
||||
Permissions.DELETE_BOOKMARK_PUBLIC_CHANNEL,
|
||||
Permissions.DELETE_BOOKMARK_PRIVATE_CHANNEL,
|
||||
);
|
||||
};
|
||||
|
||||
export const getChannelBookmarkById = async (database: Database, bookmarkId: string) => {
|
||||
try {
|
||||
const bookmark = await database.get<ChannelBookmarkModel>(CHANNEL_BOOKMARK).find(bookmarkId);
|
||||
return bookmark;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const queryBookmarks = (database: Database, channelId: string) => {
|
||||
return database.get<ChannelBookmarkModel>(CHANNEL_BOOKMARK).query(
|
||||
Q.and(
|
||||
Q.where('channel_id', channelId),
|
||||
Q.where('delete_at', Q.eq(0)),
|
||||
),
|
||||
Q.sortBy('sort_order', Q.asc),
|
||||
);
|
||||
};
|
||||
|
||||
export const getBookmarksSince = async (database: Database, channelId: string) => {
|
||||
try {
|
||||
const result = await database.get(CHANNEL_BOOKMARK).query(
|
||||
Q.unsafeSqlQuery(
|
||||
`SELECT
|
||||
COALESCE(
|
||||
MAX (
|
||||
MAX(COALESCE(create_at, 0)),
|
||||
MAX(COALESCE(update_at, 0)),
|
||||
MAX(COALESCE(delete_at, 0))
|
||||
) + 1, 0) as mostRecent
|
||||
FROM ChannelBookmark
|
||||
WHERE channel_id='${channelId}'`),
|
||||
).unsafeFetchRaw();
|
||||
|
||||
return result?.[0]?.mostRecent ?? 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const observeBookmarks = (database: Database, channelId: string) => {
|
||||
return queryBookmarks(database, channelId).observeWithColumns(['file_id']);
|
||||
};
|
||||
|
|
@ -33,7 +33,6 @@ type PrepareModelsArgs = {
|
|||
}
|
||||
|
||||
const {
|
||||
CHANNEL_BOOKMARK,
|
||||
POST,
|
||||
POSTS_IN_CHANNEL,
|
||||
POSTS_IN_THREAD,
|
||||
|
|
@ -101,7 +100,6 @@ export async function truncateCrtRelatedTables(serverUrl: string): Promise<{erro
|
|||
[`DELETE FROM ${THREAD_PARTICIPANT}`, []],
|
||||
[`DELETE FROM ${TEAM_THREADS_SYNC}`, []],
|
||||
[`DELETE FROM ${MY_CHANNEL}`, []],
|
||||
[`DELETE FROM ${CHANNEL_BOOKMARK}`, []],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
|
|
@ -20,12 +18,6 @@ export const getFileById = async (database: Database, fileId: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const observeFileById = (database: Database, id: string) => {
|
||||
return database.get<FileModel>(FILE).query(Q.where('id', id), Q.take(1)).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$(undefined))),
|
||||
);
|
||||
};
|
||||
|
||||
export const queryFilesForPost = (database: Database, postId: string) => {
|
||||
return database.get<FileModel>(FILE).query(
|
||||
Q.where('post_id', postId),
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ type ChannelProps = {
|
|||
currentUserId: string;
|
||||
channelType: ChannelType;
|
||||
hasGMasDMFeature: boolean;
|
||||
includeBookmarkBar?: boolean;
|
||||
};
|
||||
|
||||
const edges: Edge[] = ['left', 'right'];
|
||||
|
|
@ -64,7 +63,6 @@ const Channel = ({
|
|||
channelType,
|
||||
currentUserId,
|
||||
hasGMasDMFeature,
|
||||
includeBookmarkBar,
|
||||
}: ChannelProps) => {
|
||||
useGMasDMNotice(currentUserId, channelType, dismissedGMasDMNotice, hasGMasDMFeature);
|
||||
const isTablet = useIsTablet();
|
||||
|
|
@ -126,7 +124,6 @@ const Channel = ({
|
|||
componentId={componentId}
|
||||
callsEnabledInChannel={isCallsEnabledInChannel}
|
||||
isTabletView={isTabletView}
|
||||
shouldRenderBookmarks={shouldRender}
|
||||
/>
|
||||
{shouldRender &&
|
||||
<>
|
||||
|
|
@ -148,13 +145,12 @@ const Channel = ({
|
|||
/>
|
||||
</>
|
||||
}
|
||||
{showFloatingCallContainer && shouldRender &&
|
||||
{showFloatingCallContainer &&
|
||||
<FloatingCallContainer
|
||||
channelId={channelId}
|
||||
showJoinCallBanner={showJoinCallBanner}
|
||||
showIncomingCalls={showIncomingCalls}
|
||||
isInACall={isInACall}
|
||||
includeBookmarkBar={includeBookmarkBar}
|
||||
/>
|
||||
}
|
||||
</SafeAreaView>
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import ChannelBookmarks from '@components/channel_bookmarks';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
canAddBookmarks: boolean;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
},
|
||||
content: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
},
|
||||
separatorContainer: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
zIndex: 1,
|
||||
},
|
||||
padding: {
|
||||
paddingTop: 2,
|
||||
},
|
||||
paddingHorizontal: {
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
}));
|
||||
|
||||
const ChannelHeaderBookmarks = ({canAddBookmarks, channelId}: Props) => {
|
||||
const theme = useTheme();
|
||||
const defaultHeight = useDefaultHeaderHeight();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const containerStyle = useMemo(() => ({
|
||||
...styles.content,
|
||||
top: defaultHeight,
|
||||
zIndex: 1,
|
||||
}), [defaultHeight]);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.paddingHorizontal}>
|
||||
<ChannelBookmarks
|
||||
channelId={channelId}
|
||||
showInInfo={false}
|
||||
canAddBookmarks={canAddBookmarks}
|
||||
separator={false}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.separatorContainer}>
|
||||
<View style={styles.separator}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelHeaderBookmarks;
|
||||
|
|
@ -24,21 +24,17 @@ import {preventDoubleTap} from '@utils/tap';
|
|||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import ChannelHeaderBookmarks from './bookmarks';
|
||||
import QuickActions, {MARGIN, SEPARATOR_HEIGHT} from './quick_actions';
|
||||
|
||||
import type {HeaderRightButton} from '@components/navigation_header/header';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type ChannelProps = {
|
||||
canAddBookmarks: boolean;
|
||||
channelId: string;
|
||||
channelType: ChannelType;
|
||||
customStatus?: UserCustomStatus;
|
||||
isBookmarksEnabled: boolean;
|
||||
isCustomStatusEnabled: boolean;
|
||||
isCustomStatusExpired: boolean;
|
||||
hasBookmarks: boolean;
|
||||
componentId?: AvailableScreens;
|
||||
displayName: string;
|
||||
isOwnDirectMessage: boolean;
|
||||
|
|
@ -47,7 +43,6 @@ type ChannelProps = {
|
|||
teamId: string;
|
||||
callsEnabledInChannel: boolean;
|
||||
isTabletView?: boolean;
|
||||
shouldRenderBookmarks: boolean;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
|
|
@ -76,9 +71,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
}));
|
||||
|
||||
const ChannelHeader = ({
|
||||
canAddBookmarks, channelId, channelType, componentId, customStatus, displayName, hasBookmarks,
|
||||
isBookmarksEnabled, isCustomStatusEnabled, isCustomStatusExpired, isOwnDirectMessage, memberCount,
|
||||
searchTerm, teamId, callsEnabledInChannel, isTabletView, shouldRenderBookmarks,
|
||||
channelId, channelType, componentId, customStatus, displayName,
|
||||
isCustomStatusEnabled, isCustomStatusExpired, isOwnDirectMessage, memberCount,
|
||||
searchTerm, teamId, callsEnabledInChannel, isTabletView,
|
||||
}: ChannelProps) => {
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
|
|
@ -249,12 +244,6 @@ const ChannelHeader = ({
|
|||
<View style={contextStyle}>
|
||||
<RoundedHeaderContext/>
|
||||
</View>
|
||||
{isBookmarksEnabled && hasBookmarks && shouldRenderBookmarks &&
|
||||
<ChannelHeaderBookmarks
|
||||
canAddBookmarks={canAddBookmarks}
|
||||
channelId={channelId}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@
|
|||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import React from 'react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {combineLatestWith, distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
import {combineLatestWith, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {General} from '@constants';
|
||||
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
|
||||
import {observeCanAddBookmarks, queryBookmarks} from '@queries/servers/channel_bookmark';
|
||||
import {observeConfigBooleanValue, observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeUser} from '@queries/servers/user';
|
||||
import {
|
||||
|
|
@ -78,21 +77,11 @@ const enhanced = withObservables(['channelId'], ({channelId, database}: OwnProps
|
|||
const memberCount = channelInfo.pipe(
|
||||
combineLatestWith(dmUser),
|
||||
switchMap(([ci, dm]) => of$(dm ? undefined : ci?.memberCount)));
|
||||
const hasBookmarks = queryBookmarks(database, channelId).observeCount(false).pipe(
|
||||
switchMap((count) => of$(count > 0)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const isBookmarksEnabled = observeConfigBooleanValue(database, 'FeatureFlagChannelBookmarks');
|
||||
const canAddBookmarks = observeCanAddBookmarks(database, channelId);
|
||||
|
||||
return {
|
||||
canAddBookmarks,
|
||||
channelType,
|
||||
customStatus,
|
||||
displayName,
|
||||
hasBookmarks,
|
||||
isBookmarksEnabled,
|
||||
isCustomStatusEnabled,
|
||||
isCustomStatusExpired,
|
||||
isOwnDirectMessage,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
import {combineLatestWith, distinctUntilChanged, of as of$, switchMap} from 'rxjs';
|
||||
import {of as of$, switchMap} from 'rxjs';
|
||||
|
||||
import {observeCallStateInChannel, observeIsCallsEnabledInChannel} from '@calls/observers';
|
||||
import {Preferences} from '@constants';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import {observeCurrentChannel} from '@queries/servers/channel';
|
||||
import {queryBookmarks} from '@queries/servers/channel_bookmark';
|
||||
import {observeHasGMasDMFeature} from '@queries/servers/features';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfigBooleanValue, observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
|
||||
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import Channel from './channel';
|
||||
|
||||
|
|
@ -27,21 +26,6 @@ const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => {
|
|||
const channelType = observeCurrentChannel(database).pipe(switchMap((c) => of$(c?.type)));
|
||||
const currentUserId = observeCurrentUserId(database);
|
||||
const hasGMasDMFeature = observeHasGMasDMFeature(database);
|
||||
const isBookmarksEnabled = observeConfigBooleanValue(database, 'FeatureFlagChannelBookmarks');
|
||||
const hasBookmarks = (count: number) => of$(count > 0);
|
||||
const includeBookmarkBar = channelId.pipe(
|
||||
combineLatestWith(isBookmarksEnabled),
|
||||
switchMap(([cId, enabled]) => {
|
||||
if (!enabled) {
|
||||
return of$(false);
|
||||
}
|
||||
|
||||
return queryBookmarks(database, cId).observeCount(false).pipe(
|
||||
switchMap(hasBookmarks),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
channelId,
|
||||
|
|
@ -51,7 +35,6 @@ const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => {
|
|||
channelType,
|
||||
currentUserId,
|
||||
hasGMasDMFeature,
|
||||
includeBookmarkBar,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {TextInput, View} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
|
||||
import BookmarkIcon from '@components/channel_bookmarks/channel_bookmark/bookmark_icon';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {openAsBottomSheet} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
emoji?: string;
|
||||
file?: ExtractedFileInfo;
|
||||
imageUrl?: string;
|
||||
setBookmarkDisplayName: (displayName: string) => void;
|
||||
setBookmarkEmoji: (emoji?: string) => void;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 8,
|
||||
...typography('Heading', 100, 'SemiBold'),
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
disabled: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
},
|
||||
iconContainer: {
|
||||
borderWidth: 1,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 8,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
borderRightWidth: 0,
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
imageContainer: {width: 28, height: 28, marginRight: 2},
|
||||
image: {width: 24, height: 24},
|
||||
input: {
|
||||
borderBottomRightRadius: 4,
|
||||
borderTopRightRadius: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
flex: 1,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200),
|
||||
lineHeight: undefined,
|
||||
},
|
||||
genericBookmark: {
|
||||
alignSelf: 'center',
|
||||
top: 2,
|
||||
},
|
||||
}));
|
||||
|
||||
const BookmarkDetail = ({disabled, emoji, file, imageUrl, setBookmarkDisplayName, setBookmarkEmoji, title}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const paddingStyle = useMemo(() => ({paddingHorizontal: isTablet ? 42 : 0}), [isTablet]);
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const openEmojiPicker = useCallback(() => {
|
||||
openAsBottomSheet({
|
||||
closeButtonId: 'close-add-emoji',
|
||||
screen: Screens.EMOJI_PICKER,
|
||||
theme,
|
||||
title: intl.formatMessage({id: 'channel_bookmark.add.emoji', defaultMessage: 'Add emoji'}),
|
||||
props: {
|
||||
onEmojiPress: setBookmarkEmoji,
|
||||
imageUrl,
|
||||
file,
|
||||
},
|
||||
});
|
||||
}, [imageUrl, file, theme, setBookmarkEmoji]);
|
||||
|
||||
return (
|
||||
<View style={paddingStyle}>
|
||||
<FormattedText
|
||||
id='channel_bookmark.add.detail_title'
|
||||
defaultMessage='Title'
|
||||
style={styles.title}
|
||||
/>
|
||||
<View style={[styles.container, disabled && styles.disabled]}>
|
||||
<Button
|
||||
containerStyle={styles.iconContainer}
|
||||
onPress={openEmojiPicker}
|
||||
>
|
||||
<View style={styles.imageContainer}>
|
||||
<BookmarkIcon
|
||||
emoji={emoji}
|
||||
emojiSize={22}
|
||||
file={file}
|
||||
genericStyle={styles.genericBookmark}
|
||||
iconSize={30}
|
||||
imageStyle={styles.image}
|
||||
imageUrl={imageUrl}
|
||||
/>
|
||||
</View>
|
||||
<CompassIcon
|
||||
size={12}
|
||||
color={theme.centerChannelColor}
|
||||
name='chevron-down'
|
||||
/>
|
||||
</Button>
|
||||
<TextInput
|
||||
editable={!disabled}
|
||||
onChangeText={setBookmarkDisplayName}
|
||||
value={title}
|
||||
style={styles.input}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkDetail;
|
||||
|
|
@ -1,360 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View, Text, Platform, type Insets} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
import {Shadow} from 'react-native-shadow-2';
|
||||
|
||||
import {uploadFile} from '@actions/remote/file';
|
||||
import CompassIcon from '@app/components/compass_icon';
|
||||
import FileIcon from '@app/components/files/file_icon';
|
||||
import ProgressBar from '@app/components/progress_bar';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {fileSizeWarning, getExtensionFromMime, getFormattedFileSize} from '@utils/file';
|
||||
import PickerUtil from '@utils/file/file_picker';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type {ClientResponse} from '@mattermost/react-native-network-client';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
close: () => void;
|
||||
disabled: boolean;
|
||||
initialFile?: FileInfo;
|
||||
maxFileSize: number;
|
||||
setBookmark: (file: ExtractedFileInfo) => void;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
viewContainer: {
|
||||
marginTop: 32,
|
||||
marginBottom: 24,
|
||||
width: '100%',
|
||||
flex: 0,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 8,
|
||||
...typography('Heading', 100, 'SemiBold'),
|
||||
},
|
||||
shadowContainer: {
|
||||
alignItems: 'center',
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
fileContainer: {
|
||||
height: 64,
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
fileInfoContainer: {
|
||||
paddingHorizontal: 16,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
filename: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
},
|
||||
fileInfo: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
textTransform: 'uppercase',
|
||||
...typography('Body', 75),
|
||||
},
|
||||
uploadError: {
|
||||
color: theme.errorTextColor,
|
||||
...typography('Body', 75),
|
||||
},
|
||||
retry: {
|
||||
paddingRight: 20,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
removeContainer: {
|
||||
position: 'absolute',
|
||||
elevation: 11,
|
||||
top: -18,
|
||||
right: -12,
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
removeButton: {
|
||||
borderRadius: 12,
|
||||
alignSelf: 'center',
|
||||
marginTop: Platform.select({
|
||||
ios: 5.4,
|
||||
android: 4.75,
|
||||
}),
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
width: 24,
|
||||
height: 25,
|
||||
},
|
||||
uploading: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
...typography('Body', 75),
|
||||
},
|
||||
progressContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
bottom: 2,
|
||||
borderBottomRightRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
},
|
||||
progress: {
|
||||
borderRadius: 4,
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
const shadowSides = {top: false, bottom: true, end: true, start: false};
|
||||
const hitSlop: Insets = {top: 10, bottom: 10, left: 10, right: 10};
|
||||
|
||||
const BookmarkFile = ({channelId, close, disabled, initialFile, maxFileSize, setBookmark}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const serverUrl = useServerUrl();
|
||||
const [file, setFile] = useState<ExtractedFileInfo|undefined>(initialFile);
|
||||
const [error, setError] = useState('');
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const styles = getStyleSheet(theme);
|
||||
const subContainerStyle = useMemo(() => [styles.viewContainer, {paddingHorizontal: isTablet ? 42 : 0}], [isTablet]);
|
||||
const cancelUpload = useRef<() => void | undefined>();
|
||||
|
||||
const onProgress = useCallback((p: number, bytes: number) => {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const f: ExtractedFileInfo = {...file};
|
||||
f.bytesRead = bytes;
|
||||
|
||||
setProgress(p);
|
||||
setFile(f);
|
||||
}, []);
|
||||
|
||||
const onComplete = useCallback((response: ClientResponse) => {
|
||||
cancelUpload.current = undefined;
|
||||
if (response.code !== 201 || !response.data) {
|
||||
setUploadError();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = response.data.file_infos as FileInfo[] | undefined;
|
||||
if (!data?.length) {
|
||||
setUploadError();
|
||||
return;
|
||||
}
|
||||
|
||||
const fileInfo = data[0];
|
||||
setFile(fileInfo);
|
||||
setBookmark(fileInfo);
|
||||
setUploading(false);
|
||||
setProgress(1);
|
||||
setFailed(false);
|
||||
setError('');
|
||||
}, []);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
cancelUpload.current = undefined;
|
||||
setUploadError();
|
||||
}, []);
|
||||
|
||||
const setUploadError = useCallback(() => {
|
||||
setProgress(0);
|
||||
setUploading(false);
|
||||
setFailed(true);
|
||||
|
||||
setError(intl.formatMessage({
|
||||
id: 'channel_bookmark.add.file_upload_error',
|
||||
defaultMessage: 'Error uploading file. Please try again.',
|
||||
}));
|
||||
}, [file, intl]);
|
||||
|
||||
const startUpload = useCallback((fileInfo: FileInfo | ExtractedFileInfo) => {
|
||||
setUploading(true);
|
||||
setProgress(0);
|
||||
|
||||
const {cancel, error: uploadError} = uploadFile(
|
||||
serverUrl,
|
||||
fileInfo,
|
||||
channelId,
|
||||
onProgress,
|
||||
onComplete,
|
||||
onError,
|
||||
fileInfo.bytesRead,
|
||||
true,
|
||||
);
|
||||
|
||||
if (cancel) {
|
||||
cancelUpload.current = cancel;
|
||||
}
|
||||
|
||||
if (uploadError) {
|
||||
setUploadError();
|
||||
cancelUpload.current?.();
|
||||
}
|
||||
}, [channelId, onProgress, onComplete, onError, serverUrl]);
|
||||
|
||||
const browseFile = useCallback(async () => {
|
||||
const picker = new PickerUtil(intl, (files) => {
|
||||
if (files.length) {
|
||||
const f = files[0];
|
||||
const extension = getExtensionFromMime(f.mime_type) || '';
|
||||
const fileWithExtension: ExtractedFileInfo = {...f, extension};
|
||||
setFile(fileWithExtension);
|
||||
startUpload(fileWithExtension);
|
||||
}
|
||||
});
|
||||
|
||||
const res = await picker.attachFileFromFiles(undefined, false);
|
||||
if (res.error) {
|
||||
close();
|
||||
}
|
||||
}, [close, startUpload]);
|
||||
|
||||
const removeAndUpload = useCallback(() => {
|
||||
cancelUpload.current?.();
|
||||
browseFile();
|
||||
}, [file, browseFile]);
|
||||
|
||||
const retry = useCallback(() => {
|
||||
cancelUpload.current?.();
|
||||
if (file) {
|
||||
startUpload(file);
|
||||
}
|
||||
}, [file, startUpload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialFile) {
|
||||
browseFile();
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelUpload.current?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (uploading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file?.id && (file?.size || 0) > maxFileSize) {
|
||||
setError(fileSizeWarning(intl, maxFileSize));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file?.id && file?.name) {
|
||||
setBookmark(file);
|
||||
}
|
||||
}, [file, intl, maxFileSize, uploading]);
|
||||
|
||||
let info;
|
||||
if (error) {
|
||||
info = (
|
||||
<Text style={styles.uploadError}>
|
||||
{error}
|
||||
</Text>
|
||||
);
|
||||
} else if (uploading) {
|
||||
info = (
|
||||
<FormattedText
|
||||
id='channel_bookmark.add.file_uploading'
|
||||
defaultMessage='Uploading... ({progress}%)'
|
||||
values={{progress: Math.ceil(progress * 100)}}
|
||||
style={styles.uploading}
|
||||
/>
|
||||
);
|
||||
} else if (file) {
|
||||
info = (
|
||||
<Text style={styles.fileInfo}>
|
||||
{`${file.extension} ${getFormattedFileSize(file.size || 0)}`}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (file) {
|
||||
return (
|
||||
<View style={subContainerStyle}>
|
||||
<FormattedText
|
||||
id='channel_bookmark.add.file_title'
|
||||
defaultMessage='Attachment'
|
||||
style={styles.title}
|
||||
/>
|
||||
<Shadow
|
||||
style={styles.shadowContainer}
|
||||
startColor='rgba(61, 60, 64, 0.08)'
|
||||
distance={4}
|
||||
sides={shadowSides}
|
||||
>
|
||||
<View style={styles.fileContainer}>
|
||||
<FileIcon file={file}/>
|
||||
<View style={styles.fileInfoContainer}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={styles.filename}
|
||||
>
|
||||
{decodeURIComponent(file.name.trim())}
|
||||
</Text>
|
||||
{info}
|
||||
</View>
|
||||
{failed &&
|
||||
<Button
|
||||
onPress={retry}
|
||||
containerStyle={styles.retry}
|
||||
>
|
||||
<CompassIcon
|
||||
color={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
name='refresh'
|
||||
size={20}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
</View>
|
||||
<TouchableWithFeedback
|
||||
disabled={disabled}
|
||||
hitSlop={hitSlop}
|
||||
style={styles.removeContainer}
|
||||
onPress={removeAndUpload}
|
||||
type='opacity'
|
||||
>
|
||||
<View style={styles.removeButton}>
|
||||
<CompassIcon
|
||||
name='close-circle'
|
||||
color={changeOpacity(theme.centerChannelColor, disabled ? 0.16 : 0.56)}
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
</Shadow>
|
||||
{uploading &&
|
||||
<View style={styles.progressContainer}>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
color={theme.buttonBg}
|
||||
style={styles.progress}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default BookmarkFile;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
|
||||
import {DEFAULT_SERVER_MAX_FILE_SIZE} from '@constants/post_draft';
|
||||
import {observeConfigIntValue} from '@queries/servers/system';
|
||||
|
||||
import BookmarkFile from './bookmark_file';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
maxFileSize: observeConfigIntValue(database, 'MaxFileSize', DEFAULT_SERVER_MAX_FILE_SIZE),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(BookmarkFile));
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Platform, View} from 'react-native';
|
||||
|
||||
import {useIsTablet} from '@app/hooks/device';
|
||||
import FloatingTextInput from '@components/floating_text_input_label';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Loading from '@components/loading';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {fetchOpenGraph} from '@utils/opengraph';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {getUrlAfterRedirect} from '@utils/url';
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
initialUrl?: string;
|
||||
resetBookmark: () => void;
|
||||
setBookmark: (url: string, title: string, imageUrl: string) => void;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
viewContainer: {
|
||||
marginVertical: 32,
|
||||
width: '100%',
|
||||
},
|
||||
description: {
|
||||
marginTop: 8,
|
||||
},
|
||||
descriptionText: {
|
||||
...typography('Body', 75),
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
},
|
||||
loading: {
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const BookmarkLink = ({disabled, initialUrl = '', resetBookmark, setBookmark}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const [error, setError] = useState('');
|
||||
const [url, setUrl] = useState(initialUrl);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const styles = getStyleSheet(theme);
|
||||
const keyboard = (Platform.OS === 'android') ? 'default' : 'url';
|
||||
const subContainerStyle = useMemo(() => [styles.viewContainer, {paddingHorizontal: isTablet ? 42 : 0}], [isTablet, styles]);
|
||||
const descContainer = useMemo(() => [styles.description, {paddingHorizontal: isTablet ? 42 : 0}], [isTablet, styles]);
|
||||
|
||||
const validateAndFetchOG = useCallback(debounce(async (text: string) => {
|
||||
setLoading(true);
|
||||
let link = await getUrlAfterRedirect(text, false);
|
||||
|
||||
if (link.error) {
|
||||
link = await getUrlAfterRedirect(text, true);
|
||||
}
|
||||
|
||||
if (link.url) {
|
||||
const result = await fetchOpenGraph(link.url, true);
|
||||
const title = result.title || text;
|
||||
const imageUrl = result.favIcon || result.imageURL || '';
|
||||
setLoading(false);
|
||||
setBookmark(link.url, title, imageUrl);
|
||||
return;
|
||||
}
|
||||
setError(intl.formatMessage({
|
||||
id: 'channel_bookmark_add.link.invalid',
|
||||
defaultMessage: 'Please enter a valid link',
|
||||
}));
|
||||
setLoading(false);
|
||||
}, 500), [intl]);
|
||||
|
||||
const onChangeText = useCallback((text: string) => {
|
||||
resetBookmark();
|
||||
setUrl(text);
|
||||
setError('');
|
||||
}, [resetBookmark]);
|
||||
|
||||
const onSubmitEditing = useCallback(() => {
|
||||
if (url) {
|
||||
validateAndFetchOG(url);
|
||||
}
|
||||
}, [url, error]);
|
||||
|
||||
useDidUpdate(debounce(() => {
|
||||
onSubmitEditing();
|
||||
}, 300), [onSubmitEditing]);
|
||||
|
||||
return (
|
||||
<View style={subContainerStyle}>
|
||||
<FloatingTextInput
|
||||
autoCapitalize={'none'}
|
||||
autoCorrect={false}
|
||||
disableFullscreenUI={true}
|
||||
editable={!disabled}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
keyboardType={keyboard}
|
||||
returnKeyType='go'
|
||||
label={intl.formatMessage({id: 'channel_bookmark_add.link', defaultMessage: 'Link'})}
|
||||
onChangeText={onChangeText}
|
||||
theme={theme}
|
||||
error={error}
|
||||
showErrorIcon={true}
|
||||
value={url}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
endAdornment={loading &&
|
||||
<Loading
|
||||
size='small'
|
||||
color={theme.buttonBg}
|
||||
containerStyle={styles.loading}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<View style={descContainer}>
|
||||
<FormattedText
|
||||
id='channel_bookmark_add.link.input.description'
|
||||
defaultMessage='Add a link to any post, file, or any external link'
|
||||
style={styles.descriptionText}
|
||||
testID='channel_bookmark_add.link.input.description'
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkLink;
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert, View, type AlertButton} from 'react-native';
|
||||
import {SafeAreaView, type Edge} from 'react-native-safe-area-context';
|
||||
|
||||
import {addRecentReaction} from '@actions/local/reactions';
|
||||
import {createChannelBookmark, deleteChannelBookmark, editChannelBookmark} from '@actions/remote/channel_bookmark';
|
||||
import Button from '@components/button';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import BookmarkDetail from './components/bookmark_detail';
|
||||
import AddBookmarkFile from './components/bookmark_file';
|
||||
import BookmarkLink from './components/bookmark_link';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
bookmark?: ChannelBookmark;
|
||||
canDeleteBookmarks?: boolean;
|
||||
channelId: string;
|
||||
closeButtonId: string;
|
||||
componentId: AvailableScreens;
|
||||
file?: FileInfo;
|
||||
ownerId: string;
|
||||
type: ChannelBookmarkType;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
progress: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
borderRadius: 4,
|
||||
paddingLeft: 3,
|
||||
marginTop: 12,
|
||||
},
|
||||
deleteBg: {backgroundColor: changeOpacity(theme.errorTextColor, 0.16)},
|
||||
deleteContainer: {paddingTop: 32},
|
||||
deleteText: {color: theme.errorTextColor},
|
||||
}));
|
||||
|
||||
const RIGHT_BUTTON = buildNavigationButton('edit-bookmark', 'channel_bookmark.edit.save_button');
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
const emptyBookmark: ChannelBookmark = {
|
||||
id: '',
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
delete_at: 0,
|
||||
channel_id: '',
|
||||
owner_id: '',
|
||||
display_name: '',
|
||||
sort_order: 0,
|
||||
type: 'link',
|
||||
};
|
||||
|
||||
const ChannelBookmarkAddOrEdit = ({
|
||||
bookmark: original,
|
||||
canDeleteBookmarks = false,
|
||||
channelId,
|
||||
closeButtonId,
|
||||
componentId,
|
||||
file: originalFile,
|
||||
ownerId,
|
||||
type,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const styles = getStyleSheet(theme);
|
||||
const [bookmark, setBookmark] = useState<ChannelBookmark|undefined>(original);
|
||||
const [file, setFile] = useState<ExtractedFileInfo|undefined>(originalFile);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const enableSaveButton = useCallback((enabled: boolean) => {
|
||||
setButtons(componentId, {
|
||||
rightButtons: [{
|
||||
...RIGHT_BUTTON,
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: formatMessage({id: 'channel_bookmark.edit.save_button', defaultMessage: 'Save'}),
|
||||
enabled,
|
||||
}],
|
||||
});
|
||||
}, [formatMessage, theme]);
|
||||
|
||||
const setBookmarkToSave = useCallback((b?: ChannelBookmark) => {
|
||||
enableSaveButton((b?.type === 'link' && Boolean(b?.link_url)) || (b?.type === 'file' && Boolean(b.file_id)));
|
||||
setBookmark(b);
|
||||
}, []);
|
||||
|
||||
const handleError = useCallback((error: string, buttons?: AlertButton[]) => {
|
||||
const title = original ? formatMessage({id: 'channel_bookmark.edit.failed_title', defaultMessage: 'Error editing bookmark'}) : formatMessage({id: 'channel_bookmark.add.failed_title', defaultMessage: 'Error adding bookmark'});
|
||||
Alert.alert(
|
||||
title,
|
||||
formatMessage({
|
||||
id: 'channel_bookmark.add_edit.failed_desc',
|
||||
defaultMessage: 'Details: {error}',
|
||||
}, {error}),
|
||||
buttons,
|
||||
);
|
||||
setIsSaving(false);
|
||||
const enabled = Boolean(bookmark?.display_name &&
|
||||
((bookmark?.type === 'link' && Boolean(bookmark?.link_url)) || (bookmark?.type === 'file' && Boolean(bookmark.file_id))));
|
||||
enableSaveButton(enabled);
|
||||
}, [bookmark, enableSaveButton, formatMessage]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
return dismissModal({componentId});
|
||||
}, [componentId]);
|
||||
|
||||
const createBookmark = useCallback(async (b: ChannelBookmark) => {
|
||||
const res = await createChannelBookmark(serverUrl, channelId, b);
|
||||
if (res.bookmark) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
handleError((res.error as Error).message);
|
||||
}, [channelId, handleError, serverUrl]);
|
||||
|
||||
const updateBookmark = useCallback(async (b: ChannelBookmark) => {
|
||||
const res = await editChannelBookmark(serverUrl, b);
|
||||
if (res.bookmarks) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
handleError((res.error as Error).message);
|
||||
}, [handleError, serverUrl]);
|
||||
|
||||
const setLinkBookmark = useCallback((url: string, title: string, imageUrl: string) => {
|
||||
const b: ChannelBookmark = {
|
||||
...(bookmark || emptyBookmark),
|
||||
owner_id: ownerId,
|
||||
channel_id: channelId,
|
||||
link_url: url,
|
||||
image_url: imageUrl,
|
||||
display_name: title,
|
||||
type: 'link',
|
||||
};
|
||||
|
||||
setBookmarkToSave(b);
|
||||
}, [bookmark, channelId, setBookmarkToSave, ownerId]);
|
||||
|
||||
const setFileBookmark = useCallback((f: ExtractedFileInfo) => {
|
||||
const b: ChannelBookmark = {
|
||||
...(bookmark || emptyBookmark),
|
||||
owner_id: ownerId,
|
||||
channel_id: channelId,
|
||||
display_name: decodeURIComponent(f.name),
|
||||
type: 'file',
|
||||
file_id: f.id,
|
||||
};
|
||||
setBookmarkToSave(b);
|
||||
setFile(f);
|
||||
}, [bookmark, channelId, ownerId]);
|
||||
|
||||
const setBookmarkDisplayName = useCallback((displayName: string) => {
|
||||
if (bookmark) {
|
||||
setBookmark((prev) => ({
|
||||
...(prev!),
|
||||
display_name: displayName,
|
||||
}));
|
||||
}
|
||||
|
||||
enableSaveButton(Boolean(displayName));
|
||||
}, [bookmark, enableSaveButton]);
|
||||
|
||||
const setBookmarkEmoji = useCallback((emoji?: string) => {
|
||||
if (bookmark) {
|
||||
setBookmark((prev) => ({
|
||||
...prev!,
|
||||
emoji,
|
||||
}));
|
||||
|
||||
const prevEmoji = original ? original.emoji : '';
|
||||
if (prevEmoji !== emoji) {
|
||||
enableSaveButton(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (emoji) {
|
||||
addRecentReaction(serverUrl, [emoji]);
|
||||
}
|
||||
}, [bookmark, enableSaveButton, serverUrl]);
|
||||
|
||||
const resetBookmark = useCallback(() => {
|
||||
setBookmarkToSave(original);
|
||||
setFile(originalFile);
|
||||
}, [setBookmarkToSave]);
|
||||
|
||||
const onSaveBookmark = useCallback(async () => {
|
||||
if (bookmark) {
|
||||
enableSaveButton(false);
|
||||
setIsSaving(true);
|
||||
if (original) {
|
||||
updateBookmark(bookmark);
|
||||
return;
|
||||
}
|
||||
|
||||
createBookmark(bookmark);
|
||||
}
|
||||
}, [bookmark, createBookmark, updateBookmark]);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (bookmark) {
|
||||
setIsSaving(true);
|
||||
enableSaveButton(false);
|
||||
const {error} = await deleteChannelBookmark(serverUrl, bookmark.channel_id, bookmark.id);
|
||||
if (error) {
|
||||
Alert.alert(
|
||||
formatMessage({id: 'channel_bookmark.delete.failed_title', defaultMessage: 'Error deleting bookmark'}),
|
||||
formatMessage({
|
||||
id: 'channel_bookmark.add_edit.failed_desc',
|
||||
defaultMessage: 'Details: {error}',
|
||||
}, {error: getFullErrorMessage(error)}),
|
||||
);
|
||||
setIsSaving(false);
|
||||
enableSaveButton(true);
|
||||
return;
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
}, [bookmark, serverUrl, close]);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
if (bookmark) {
|
||||
Alert.alert(
|
||||
formatMessage({id: 'channel_bookmark.delete.confirm_title', defaultMessage: 'Delete bookmark'}),
|
||||
formatMessage({id: 'channel_bookmark.delete.confirm', defaultMessage: 'You sure want to delete the bookmark {displayName}?'}, {
|
||||
displayName: bookmark.display_name,
|
||||
}),
|
||||
[{
|
||||
text: formatMessage({id: 'channel_bookmark.delete.yes', defaultMessage: 'Yes'}),
|
||||
style: 'destructive',
|
||||
isPreferred: true,
|
||||
onPress: handleDelete,
|
||||
}, {
|
||||
text: formatMessage({id: 'channel_bookmark.add.file_cancel', defaultMessage: 'Cancel'}),
|
||||
style: 'cancel',
|
||||
}],
|
||||
);
|
||||
}
|
||||
}, [bookmark, handleDelete]);
|
||||
|
||||
useEffect(() => {
|
||||
enableSaveButton(false);
|
||||
}, []);
|
||||
|
||||
useNavButtonPressed(RIGHT_BUTTON.id, componentId, onSaveBookmark, [bookmark]);
|
||||
useNavButtonPressed(closeButtonId, componentId, close, [close]);
|
||||
useAndroidHardwareBackHandler(componentId, close);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.content}
|
||||
testID='channel_bookmark.screen'
|
||||
>
|
||||
{type === 'link' &&
|
||||
<BookmarkLink
|
||||
disabled={isSaving}
|
||||
initialUrl={original?.link_url}
|
||||
setBookmark={setLinkBookmark}
|
||||
resetBookmark={resetBookmark}
|
||||
/>
|
||||
}
|
||||
{type === 'file' &&
|
||||
<AddBookmarkFile
|
||||
channelId={channelId}
|
||||
close={close}
|
||||
disabled={isSaving}
|
||||
initialFile={originalFile}
|
||||
setBookmark={setFileBookmark}
|
||||
/>
|
||||
}
|
||||
{Boolean(bookmark) &&
|
||||
<>
|
||||
<BookmarkDetail
|
||||
disabled={isSaving}
|
||||
emoji={bookmark?.emoji}
|
||||
imageUrl={bookmark?.image_url}
|
||||
title={bookmark?.display_name || ''}
|
||||
file={file}
|
||||
setBookmarkDisplayName={setBookmarkDisplayName}
|
||||
setBookmarkEmoji={setBookmarkEmoji}
|
||||
/>
|
||||
{canDeleteBookmarks &&
|
||||
<View style={styles.deleteContainer}>
|
||||
<Button
|
||||
buttonType='destructive'
|
||||
size='m'
|
||||
text='Delete bookmark'
|
||||
iconName='trash-can-outline'
|
||||
textStyle={styles.deleteText}
|
||||
backgroundStyle={styles.deleteBg}
|
||||
iconSize={18}
|
||||
onPress={onDelete}
|
||||
theme={theme}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelBookmarkAddOrEdit;
|
||||
|
|
@ -8,7 +8,6 @@ import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
|
|||
import ChannelInfoEnableCalls from '@calls/components/channel_info_enable_calls';
|
||||
import ChannelActions from '@components/channel_actions';
|
||||
import ConvertToChannelLabel from '@components/channel_actions/convert_to_channel/convert_to_channel_label';
|
||||
import ChannelBookmarks from '@components/channel_bookmarks';
|
||||
import {General} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
|
@ -26,19 +25,17 @@ import Title from './title';
|
|||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
canAddBookmarks: boolean;
|
||||
canEnableDisableCalls: boolean;
|
||||
canManageMembers: boolean;
|
||||
canManageSettings: boolean;
|
||||
channelId: string;
|
||||
closeButtonId: string;
|
||||
componentId: AvailableScreens;
|
||||
isBookmarksEnabled: boolean;
|
||||
isCallsEnabledInChannel: boolean;
|
||||
isConvertGMFeatureAvailable: boolean;
|
||||
isCRTEnabled: boolean;
|
||||
isGuestUser: boolean;
|
||||
type?: ChannelType;
|
||||
canEnableDisableCalls: boolean;
|
||||
isCallsEnabledInChannel: boolean;
|
||||
canManageMembers: boolean;
|
||||
isCRTEnabled: boolean;
|
||||
canManageSettings: boolean;
|
||||
isGuestUser: boolean;
|
||||
isConvertGMFeatureAvailable: boolean;
|
||||
}
|
||||
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
|
@ -59,19 +56,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
}));
|
||||
|
||||
const ChannelInfo = ({
|
||||
canAddBookmarks,
|
||||
canEnableDisableCalls,
|
||||
canManageMembers,
|
||||
canManageSettings,
|
||||
isCRTEnabled,
|
||||
channelId,
|
||||
closeButtonId,
|
||||
componentId,
|
||||
isBookmarksEnabled,
|
||||
isCallsEnabledInChannel,
|
||||
isConvertGMFeatureAvailable,
|
||||
isCRTEnabled,
|
||||
isGuestUser,
|
||||
type,
|
||||
canEnableDisableCalls,
|
||||
isCallsEnabledInChannel,
|
||||
canManageMembers,
|
||||
canManageSettings,
|
||||
isGuestUser,
|
||||
isConvertGMFeatureAvailable,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -106,13 +101,6 @@ const ChannelInfo = ({
|
|||
channelId={channelId}
|
||||
type={type}
|
||||
/>
|
||||
{isBookmarksEnabled &&
|
||||
<ChannelBookmarks
|
||||
channelId={channelId}
|
||||
canAddBookmarks={canAddBookmarks}
|
||||
showInInfo={true}
|
||||
/>
|
||||
}
|
||||
<ChannelActions
|
||||
channelId={channelId}
|
||||
inModal={true}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@ import {observeIsCallsEnabledInChannel} from '@calls/observers';
|
|||
import {observeCallsConfig} from '@calls/state';
|
||||
import {withServerUrl} from '@context/server';
|
||||
import {observeCurrentChannel} from '@queries/servers/channel';
|
||||
import {observeCanAddBookmarks} from '@queries/servers/channel_bookmark';
|
||||
import {observeCanManageChannelMembers, observeCanManageChannelSettings} from '@queries/servers/role';
|
||||
import {
|
||||
observeConfigBooleanValue,
|
||||
observeConfigValue,
|
||||
observeCurrentChannelId,
|
||||
observeCurrentTeamId,
|
||||
|
|
@ -61,8 +59,7 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
|||
switchMap(([uId, chId]) => observeUserIsChannelAdmin(database, uId, chId)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const serverVersion = observeConfigValue(database, 'Version');
|
||||
const callsGAServer = serverVersion.pipe(
|
||||
const callsGAServer = observeConfigValue(database, 'Version').pipe(
|
||||
switchMap((v) => of$(isMinimumServerVersion(v || '', 7, 6))),
|
||||
);
|
||||
const dmOrGM = type.pipe(switchMap((t) => of$(isTypeDMorGM(t))));
|
||||
|
|
@ -120,27 +117,17 @@ const enhanced = withObservables([], ({serverUrl, database}: Props) => {
|
|||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const isConvertGMFeatureAvailable = serverVersion.pipe(
|
||||
const isConvertGMFeatureAvailable = observeConfigValue(database, 'Version').pipe(
|
||||
switchMap((version) => of$(isMinimumServerVersion(version || '', 9, 1))),
|
||||
);
|
||||
|
||||
const isBookmarksEnabled = observeConfigBooleanValue(database, 'FeatureFlagChannelBookmarks');
|
||||
|
||||
const canAddBookmarks = channelId.pipe(
|
||||
switchMap((cId) => {
|
||||
return observeCanAddBookmarks(database, cId);
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
type,
|
||||
canEnableDisableCalls,
|
||||
canAddBookmarks,
|
||||
canManageMembers,
|
||||
canManageSettings,
|
||||
isBookmarksEnabled,
|
||||
isCallsEnabledInChannel,
|
||||
canManageMembers,
|
||||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
canManageSettings,
|
||||
isGuestUser,
|
||||
isConvertGMFeatureAvailable,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ const Field = ({
|
|||
|
||||
const formattedLabel = isOptional ? `${label} ${optionalText}` : label;
|
||||
|
||||
const textInputStyle = isDisabled ? style.disabledStyle : undefined;
|
||||
const subContainer = [style.viewContainer, {paddingHorizontal: isTablet ? 42 : 20}];
|
||||
const fieldInputTestId = isDisabled ? `${testID}.input.disabled` : `${testID}.input`;
|
||||
|
||||
|
|
@ -96,6 +97,7 @@ const Field = ({
|
|||
value={value}
|
||||
ref={fieldRef}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
textInputStyle={textInputStyle}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import type {AvailableScreens} from '@typings/screens/navigation';
|
|||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
imageUrl?: string;
|
||||
file?: ExtractedFileInfo;
|
||||
closeButtonId: string;
|
||||
};
|
||||
|
||||
|
|
@ -26,7 +24,7 @@ const style = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
const EmojiPickerScreen = ({closeButtonId, componentId, file, imageUrl, onEmojiPress}: Props) => {
|
||||
const EmojiPickerScreen = ({closeButtonId, componentId, onEmojiPress}: Props) => {
|
||||
const handleEmojiPress = useCallback((emoji: string) => {
|
||||
onEmojiPress(emoji);
|
||||
DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET);
|
||||
|
|
@ -36,8 +34,6 @@ const EmojiPickerScreen = ({closeButtonId, componentId, file, imageUrl, onEmojiP
|
|||
return (
|
||||
<Picker
|
||||
onEmojiPress={handleEmojiPress}
|
||||
imageUrl={imageUrl}
|
||||
file={file}
|
||||
testID='emoji_picker'
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -31,13 +31,11 @@ type Props = {
|
|||
customEmojis: CustomEmojiModel[];
|
||||
customEmojisEnabled: boolean;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
imageUrl?: string;
|
||||
file?: ExtractedFileInfo;
|
||||
recentEmojis: string[];
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const Picker = ({customEmojis, customEmojisEnabled, file, imageUrl, onEmojiPress, recentEmojis, testID = ''}: Props) => {
|
||||
const Picker = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis, testID = ''}: Props) => {
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const [searchTerm, setSearchTerm] = useState<string|undefined>();
|
||||
|
|
@ -70,8 +68,6 @@ const Picker = ({customEmojis, customEmojisEnabled, file, imageUrl, onEmojiPress
|
|||
<EmojiSections
|
||||
customEmojis={customEmojis}
|
||||
customEmojisEnabled={customEmojisEnabled}
|
||||
imageUrl={imageUrl}
|
||||
file={file}
|
||||
onEmojiPress={onEmojiPress}
|
||||
recentEmojis={recentEmojis}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ import {BottomSheetSectionList} from '@gorhom/bottom-sheet';
|
|||
import {chunk} from 'lodash';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {type ListRenderItemInfo, type NativeScrollEvent, type NativeSyntheticEvent, SectionList, type SectionListData, StyleSheet, View} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
|
||||
|
||||
import {fetchCustomEmojis} from '@actions/remote/custom_emoji';
|
||||
import FileIcon from '@components/files/file_icon';
|
||||
import TouchableEmoji from '@components/touchable_emoji';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {EMOJIS_PER_PAGE} from '@constants/emoji';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
|
|
@ -68,28 +65,15 @@ const styles = StyleSheet.create(({
|
|||
height: EMOJI_SIZE,
|
||||
width: EMOJI_SIZE,
|
||||
},
|
||||
imageEmoji: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
customEmojis: CustomEmojiModel[];
|
||||
customEmojisEnabled: boolean;
|
||||
imageUrl?: string;
|
||||
file?: ExtractedFileInfo;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
recentEmojis: string[];
|
||||
}
|
||||
|
||||
type ImageEmojiProps = {
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
file?: ExtractedFileInfo;
|
||||
imageUrl?: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
CategoryNames.forEach((name: string) => {
|
||||
if (CategoryTranslations.has(name) && CategoryMessage.has(name)) {
|
||||
categoryToI18n[name] = {
|
||||
|
|
@ -106,36 +90,7 @@ const emptyEmoji: EmojiAlias = {
|
|||
aliases: [],
|
||||
};
|
||||
|
||||
const ImageEmoji = ({file, imageUrl, onEmojiPress, path}: ImageEmojiProps) => {
|
||||
const onPress = useCallback(() => {
|
||||
onEmojiPress('');
|
||||
}, [onEmojiPress]);
|
||||
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<View style={styles.emoji}>
|
||||
<TouchableWithFeedback onPress={onPress}>
|
||||
<>
|
||||
{Boolean(file) &&
|
||||
<FileIcon
|
||||
file={file}
|
||||
iconSize={30}
|
||||
/>
|
||||
}
|
||||
{Boolean(imageUrl) &&
|
||||
<FastImage
|
||||
source={{uri: path}}
|
||||
style={styles.imageEmoji}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const EmojiSections = ({customEmojis, customEmojisEnabled, file, imageUrl, onEmojiPress, recentEmojis}: Props) => {
|
||||
const EmojiSections = ({customEmojis, customEmojisEnabled, onEmojiPress, recentEmojis}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const {currentIndex, selectedIndex} = useEmojiCategoryBar();
|
||||
|
|
@ -150,7 +105,7 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, file, imageUrl, onEmo
|
|||
const sections: EmojiSection[] = useMemo(() => {
|
||||
const emojisPerRow = isTablet ? EMOJIS_PER_ROW_TABLET : EMOJIS_PER_ROW;
|
||||
|
||||
const sectionsArray = CategoryNames.map<EmojiSection>((category) => {
|
||||
return CategoryNames.map((category) => {
|
||||
const emojiIndices = EmojiIndicesByCategory.get('default')?.get(category);
|
||||
|
||||
let data: EmojiAlias[][];
|
||||
|
|
@ -195,34 +150,7 @@ const EmojiSections = ({customEmojis, customEmojisEnabled, file, imageUrl, onEmo
|
|||
key: category,
|
||||
};
|
||||
}).filter((s: EmojiSection) => s.data.length);
|
||||
|
||||
if (imageUrl || file) {
|
||||
sectionsArray.unshift({
|
||||
data: [[{
|
||||
aliases: [],
|
||||
name: imageUrl || file?.name || '',
|
||||
short_name: imageUrl || file?.name || '',
|
||||
category: 'image',
|
||||
}]],
|
||||
defaultMessage: 'Default',
|
||||
icon: 'bookmark-outline',
|
||||
id: 'emoji_picker.default',
|
||||
key: 'default',
|
||||
renderItem: ({item}: ListRenderItemInfo<EmojiAlias[]>) => {
|
||||
return (
|
||||
<ImageEmoji
|
||||
file={file}
|
||||
onEmojiPress={onEmojiPress}
|
||||
imageUrl={imageUrl}
|
||||
path={item[0].name}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return sectionsArray;
|
||||
}, [customEmojis, customEmojisEnabled, isTablet, imageUrl, file]);
|
||||
}, [customEmojis, customEmojisEnabled, isTablet]);
|
||||
|
||||
useEffect(() => {
|
||||
setEmojiCategoryBarIcons(sections.map((s) => ({
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const SectionHeader = ({section}: Props) => {
|
|||
<FormattedText
|
||||
style={styles.sectionTitle}
|
||||
id={section.id}
|
||||
defaultMessage={section.defaultMessage}
|
||||
defaultMessage={section.icon}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -78,9 +78,6 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/channel').default);
|
||||
break;
|
||||
case Screens.CHANNEL_BOOKMARK:
|
||||
screen = withServerDatabase(require('@screens/channel_bookmark').default);
|
||||
break;
|
||||
case Screens.CHANNEL_NOTIFICATION_PREFERENCES:
|
||||
screen = withServerDatabase(require('@screens/channel_notification_preferences').default);
|
||||
break;
|
||||
|
|
@ -271,9 +268,6 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.CALL:
|
||||
screen = withServerDatabase(require('@calls/screens/call_screen').default);
|
||||
break;
|
||||
case Screens.GENERIC_OVERLAY:
|
||||
screen = withServerDatabase(require('@screens/overlay').default);
|
||||
break;
|
||||
}
|
||||
|
||||
if (screen) {
|
||||
|
|
|
|||
|
|
@ -729,7 +729,7 @@ export function setButtons(componentId: AvailableScreens, buttons: NavButtons =
|
|||
mergeNavigationOptions(componentId, options);
|
||||
}
|
||||
|
||||
export function showOverlay(name: AvailableScreens, passProps = {}, options: Options = {}, id?: string) {
|
||||
export function showOverlay(name: AvailableScreens, passProps = {}, options: Options = {}) {
|
||||
if (!isScreenRegistered(name)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -746,7 +746,6 @@ export function showOverlay(name: AvailableScreens, passProps = {}, options: Opt
|
|||
|
||||
Navigation.showOverlay({
|
||||
component: {
|
||||
id,
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
|
|
@ -754,7 +753,7 @@ export function showOverlay(name: AvailableScreens, passProps = {}, options: Opt
|
|||
});
|
||||
}
|
||||
|
||||
export async function dismissOverlay(componentId: string) {
|
||||
export async function dismissOverlay(componentId: AvailableScreens) {
|
||||
try {
|
||||
await Navigation.dismissOverlay(componentId);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {type ReactNode} from 'react';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Overlay = ({children}: Props) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
export default Overlay;
|
||||
|
|
@ -260,7 +260,7 @@ export default class FilePickerUtil {
|
|||
};
|
||||
|
||||
private buildUri = async (doc: DocumentPickerResponse) => {
|
||||
let uri: string = doc.fileCopyUri || doc.uri;
|
||||
let uri: string = doc.uri;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
if (doc.fileCopyUri) {
|
||||
|
|
@ -273,9 +273,10 @@ export default class FilePickerUtil {
|
|||
return {doc: undefined};
|
||||
}
|
||||
}
|
||||
|
||||
doc.uri = uri;
|
||||
}
|
||||
|
||||
doc.uri = uri;
|
||||
return {doc};
|
||||
};
|
||||
|
||||
|
|
@ -328,13 +329,10 @@ export default class FilePickerUtil {
|
|||
);
|
||||
|
||||
await this.prepareFileUpload(docs);
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return {error: 'no permission'};
|
||||
};
|
||||
|
||||
attachFileFromPhotoGallery = async (selectionLimit = 1) => {
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ export function getFormattedFileSize(bytes: number): string {
|
|||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
export function getFileType(file: FileInfo | ExtractedFileInfo): string {
|
||||
export function getFileType(file: FileInfo): string {
|
||||
if (!file || !file.extension) {
|
||||
return 'other';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {DeviceEventEmitter, Image, Keyboard, NativeModules, Platform} from 'react-native';
|
||||
import {DeviceEventEmitter, Keyboard, NativeModules, Platform} from 'react-native';
|
||||
import {Navigation, type Options, type OptionsLayout} from 'react-native-navigation';
|
||||
import {measure, type AnimatedRef} from 'react-native-reanimated';
|
||||
|
||||
|
|
@ -176,9 +176,3 @@ export const workletNoopTrue = () => {
|
|||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getImageSize = (uri: string) => {
|
||||
return new Promise<{width: number; height: number}>((resolve, reject) => {
|
||||
Image.getSize(uri, (width, height) => resolve({width, height}), reject);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,53 +1,11 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import htmlParser from 'fast-html-parser';
|
||||
import {decode} from 'html-entities';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
export type BestImage = {
|
||||
secure_url?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export type OpenGraph = {
|
||||
link: string;
|
||||
title?: string;
|
||||
imageURL?: string;
|
||||
favIcon?: string;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
type LinkRelIcon = {
|
||||
href: string;
|
||||
sizes?: string;
|
||||
}
|
||||
|
||||
const metaTags: Record<string, string> = {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
ogUrl: 'og:url',
|
||||
ogType: 'og:type',
|
||||
ogTitle: 'og:title',
|
||||
ogDescription: 'og:description',
|
||||
ogImage: 'og:image',
|
||||
ogVideo: 'og:video',
|
||||
ogVideoType: 'og:video:type',
|
||||
ogVideoWidth: 'og:video:width',
|
||||
ogVideoHeight: 'og:video:height',
|
||||
ogVideoUrl: 'og:video:url',
|
||||
twitterPlayer: 'twitter:player',
|
||||
twitterPlayerWidth: 'twitter:player:width',
|
||||
twitterPlayerHeight: 'twitter:player:height',
|
||||
twitterPlayerStream: 'twitter:player:stream',
|
||||
twitterCard: 'twitter:card',
|
||||
twitterDomain: 'twitter:domain',
|
||||
twitterUrl: 'twitter:url',
|
||||
twitterTitle: 'twitter:title',
|
||||
twitterDescription: 'twitter:description',
|
||||
twitterImage: 'twitter:image',
|
||||
};
|
||||
|
||||
export function getDistanceBW2Points(point1: Record<string, any>, point2: Record<string, any>, xAttr = 'x', yAttr = 'y') {
|
||||
return Math.sqrt(Math.pow(point1[xAttr] - point2[xAttr], 2) + Math.pow(point1[yAttr] - point2[yAttr], 2));
|
||||
}
|
||||
|
|
@ -68,122 +26,3 @@ export function getNearestPoint(pivotPoint: {height: number; width: number}, poi
|
|||
}
|
||||
return nearestPoint as BestImage;
|
||||
}
|
||||
|
||||
const fetchRaw = async (url: string) => {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'OpenGraph',
|
||||
'Cache-Control': 'no-cache',
|
||||
Accept: '*/*',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return (await res.text()) as any;
|
||||
} catch (error: any) {
|
||||
return {message: error.message};
|
||||
}
|
||||
};
|
||||
|
||||
const getFavIcon = (url: string, html: string) => {
|
||||
const getSize = (el: LinkRelIcon) => {
|
||||
return (el.sizes && el.sizes[0] && parseInt(el.sizes[0], 10)) || 0;
|
||||
};
|
||||
|
||||
const root = htmlParser.parse(html);
|
||||
|
||||
const icons = (root.querySelectorAll('link')).reduce<LinkRelIcon[]>((r, e) => {
|
||||
if (e.attributes.rel === 'icon' || e.attributes.rel === 'shortcut icon') {
|
||||
const {href, sizes} = e.attributes;
|
||||
r.push({href, sizes});
|
||||
}
|
||||
return r;
|
||||
}, []).sort((a, b) => {
|
||||
return getSize(b) - getSize(a);
|
||||
});
|
||||
|
||||
if (icons.length) {
|
||||
const icon = icons[0].href;
|
||||
let parsed = urlParse(icon);
|
||||
if (!parsed.host) {
|
||||
parsed = urlParse(url);
|
||||
return `${parsed.protocol}//${parsed.host}${icon}`;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
const parsed = urlParse(url);
|
||||
return `${parsed.protocol}//${parsed.host}/favicon.ico`;
|
||||
};
|
||||
|
||||
export const fetchOpenGraph = async (url: string, includeFavIcon = false): Promise<OpenGraph> => {
|
||||
const {
|
||||
ogTitle,
|
||||
ogImage,
|
||||
} = metaTags;
|
||||
|
||||
try {
|
||||
const html = await fetchRaw(url);
|
||||
if (html.message) {
|
||||
throw Error(html.message);
|
||||
}
|
||||
|
||||
let siteTitle = '';
|
||||
|
||||
const tagTitle = html.match(
|
||||
/<title[^>]*>[\r\n\t\s]*([^<]+)[\r\n\t\s]*<\/title>/gim,
|
||||
);
|
||||
siteTitle = tagTitle[0].replace(
|
||||
/<title[^>]*>[\r\n\t\s]*([^<]+)[\r\n\t\s]*<\/title>/gim,
|
||||
'$1',
|
||||
);
|
||||
|
||||
const og = [];
|
||||
const metas: any = html.match(/<meta[^>]+>/gim);
|
||||
|
||||
// There is no else statement
|
||||
/* istanbul ignore else */
|
||||
if (metas) {
|
||||
for (let meta of metas) {
|
||||
meta = meta.replace(/\s*\/?>$/, ' />');
|
||||
const zname = meta.replace(/[\s\S]*(property|name)\s*=\s*([\s\S]+)/, '$2');
|
||||
const name = (/^["']/).test(zname) ? zname.substr(1, zname.slice(1).indexOf(zname[0])) : zname.substr(0, zname.search(/[\s\t]/g));
|
||||
const valid = Boolean(Object.keys(metaTags).filter((m: any) => metaTags[m].toLowerCase() === name.toLowerCase()).length);
|
||||
|
||||
// There is no else statement
|
||||
/* istanbul ignore else */
|
||||
if (valid) {
|
||||
const zcontent = meta.replace(/[\s\S]*(content)\s*=\s*([\s\S]+)/, '$2');
|
||||
const content = (/^["']/).test(zcontent) ? zcontent.substr(1, zcontent.slice(1).indexOf(zcontent[0])) : zcontent.substr(0, zcontent.search(/[\s\t]/g));
|
||||
og.push({name, value: content === 'undefined' ? null : content});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: OpenGraph = {link: url};
|
||||
const data = og.reduce(
|
||||
(chain: any, meta: any) => ({...chain, [meta.name]: decode(meta.value)}),
|
||||
{url},
|
||||
);
|
||||
|
||||
// Image
|
||||
result.imageURL = data[ogImage] ? data[ogImage] : null;
|
||||
result.favIcon = includeFavIcon ? getFavIcon(url, html) : undefined;
|
||||
|
||||
// Title
|
||||
data[ogTitle] = data[ogTitle] ? data[ogTitle] : siteTitle;
|
||||
|
||||
result.title = data[ogTitle];
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
link: url,
|
||||
error,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function isValidUrl(url = '') {
|
|||
|
||||
export function sanitizeUrl(url: string, useHttp = false) {
|
||||
let preUrl = urlParse(url, true);
|
||||
let protocol = useHttp ? 'http:' : preUrl.protocol;
|
||||
let protocol = preUrl.protocol;
|
||||
|
||||
if (!preUrl.host || preUrl.protocol === 'file:') {
|
||||
preUrl = urlParse('https://' + stripTrailingSlashes(url), true);
|
||||
|
|
@ -34,18 +34,6 @@ export function sanitizeUrl(url: string, useHttp = false) {
|
|||
);
|
||||
}
|
||||
|
||||
export async function getUrlAfterRedirect(url: string, useHttp = false) {
|
||||
const link = sanitizeUrl(url, useHttp);
|
||||
try {
|
||||
const result = await fetch(link, {
|
||||
method: 'HEAD',
|
||||
});
|
||||
return {url: result.url};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getServerUrlAfterRedirect(serverUrl: string, useHttp = false) {
|
||||
let url = sanitizeUrl(serverUrl, useHttp);
|
||||
|
||||
|
|
|
|||
|
|
@ -96,28 +96,6 @@
|
|||
"camera_type.video.option": "Record Video",
|
||||
"center_panel.archived.closeChannel": "Close Channel",
|
||||
"channel_add_members.add_members.button": "Add Members",
|
||||
"channel_bookmark_add.link": "Link",
|
||||
"channel_bookmark_add.link.input.description": "Add a link to any post, file, or any external link",
|
||||
"channel_bookmark_add.link.invalid": "Please enter a valid link",
|
||||
"channel_bookmark.add_edit.failed_desc": "Details: {error}",
|
||||
"channel_bookmark.add.detail_title": "Title",
|
||||
"channel_bookmark.add.emoji": "Add emoji",
|
||||
"channel_bookmark.add.failed_title": "Error adding bookmark",
|
||||
"channel_bookmark.add.file_cancel": "Cancel",
|
||||
"channel_bookmark.add.file_title": "Attachment",
|
||||
"channel_bookmark.add.file_upload_error": "Error uploading file. Please try again.",
|
||||
"channel_bookmark.add.file_uploading": "Uploading... ({progress}%)",
|
||||
"channel_bookmark.copy_option": "Copy Link",
|
||||
"channel_bookmark.delete_option": "Delete",
|
||||
"channel_bookmark.delete.confirm": "You sure want to delete the bookmark {displayName}?",
|
||||
"channel_bookmark.delete.confirm_title": "Delete bookmark",
|
||||
"channel_bookmark.delete.failed_detail": "Details: {error}",
|
||||
"channel_bookmark.delete.failed_title": "Error deleting bookmark",
|
||||
"channel_bookmark.delete.yes": "Yes",
|
||||
"channel_bookmark.edit_option": "Edit",
|
||||
"channel_bookmark.edit.failed_title": "Error editing bookmark",
|
||||
"channel_bookmark.edit.save_button": "Save",
|
||||
"channel_bookmark.share_option": "Share",
|
||||
"channel_files.empty.paragraph": "Files posted in this channel will show here.",
|
||||
"channel_files.empty.title": "No files yet",
|
||||
"channel_files.noFiles.paragraph": "This channel doesn't contain any files with the applied filters",
|
||||
|
|
@ -125,10 +103,6 @@
|
|||
"channel_header.directchannel.you": "{displayName} (you)",
|
||||
"channel_header.info": "View info",
|
||||
"channel_header.member_count": "{count, plural, one {# member} other {# members}}",
|
||||
"channel_info.add_bookmark": "Add a bookmark",
|
||||
"channel_info.add_bookmark.file": "Attach a file",
|
||||
"channel_info.add_bookmark.link": "Add a link",
|
||||
"channel_info.add_bookmark.max_reached": "This channel has reached the maximum number of bookmarks.",
|
||||
"channel_info.add_members": "Add members",
|
||||
"channel_info.alert_retry": "Try Again",
|
||||
"channel_info.alertNo": "No",
|
||||
|
|
@ -806,6 +780,7 @@
|
|||
"notification_settings.mention.reply": "Send reply notifications for",
|
||||
"notification_settings.mentions": "Mentions",
|
||||
"notification_settings.mentions_replies": "Mentions and Replies",
|
||||
"notification_settings.mentions..keywordsDescription": "Other words that trigger a mention",
|
||||
"notification_settings.mentions.channelWide": "Channel-wide mentions",
|
||||
"notification_settings.mentions.keywords": "Enter other keywords",
|
||||
"notification_settings.mentions.keywords_mention": "Keywords that trigger mentions",
|
||||
|
|
@ -952,8 +927,6 @@
|
|||
"screen.search.results.filter.title": "Filter by file type",
|
||||
"screen.search.results.filter.videos": "Videos",
|
||||
"screen.search.title": "Search",
|
||||
"screens.channel_bookmark_add": "Add a bookmark",
|
||||
"screens.channel_bookmark_edit": "Edit bookmark",
|
||||
"screens.channel_edit": "Edit Channel",
|
||||
"screens.channel_edit_header": "Edit Channel Header",
|
||||
"screens.channel_info": "Channel Info",
|
||||
|
|
|
|||
|
|
@ -163,12 +163,7 @@ RCT_EXPORT_METHOD(createThumbnail:(NSDictionary *)config findEventsWithResolver:
|
|||
|
||||
if ([url_ hasPrefix:@"http://"] || [url_ hasPrefix:@"https://"] || [url_ hasPrefix:@"file://"]) {
|
||||
vidURL = [NSURL URLWithString:url];
|
||||
NSString *serverUrl;
|
||||
if (vidURL.port != nil) {
|
||||
serverUrl = [NSString stringWithFormat:@"%@://%@:%@", vidURL.scheme, vidURL.host, vidURL.port];
|
||||
} else {
|
||||
serverUrl = [NSString stringWithFormat:@"%@://%@", vidURL.scheme, vidURL.host];
|
||||
}
|
||||
NSString *serverUrl = [NSString stringWithFormat:@"%@://%@:%@", vidURL.scheme, vidURL.host, vidURL.port];
|
||||
if (vidURL != nil) {
|
||||
NSString *token = [[GekidouWrapper default] getTokenFor:serverUrl];
|
||||
if (token != nil) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue