diff --git a/app/actions/app/global.ts b/app/actions/app/global.ts index d864d9b18..fde5c8d11 100644 --- a/app/actions/app/global.ts +++ b/app/actions/app/global.ts @@ -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); -}; diff --git a/app/actions/local/channel_bookmark.ts b/app/actions/local/channel_bookmark.ts deleted file mode 100644 index ff2479756..000000000 --- a/app/actions/local/channel_bookmark.ts +++ /dev/null @@ -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, 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, 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, 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}; - } -} diff --git a/app/actions/local/file.ts b/app/actions/local/file.ts index a4fdba9a7..649ef9666 100644 --- a/app/actions/local/file.ts +++ b/app/actions/local/file.ts @@ -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}; - } -}; diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index cf2aeffb0..d43c4f799 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -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); diff --git a/app/actions/remote/channel_bookmark.ts b/app/actions/remote/channel_bookmark.ts deleted file mode 100644 index 825295ad5..000000000 --- a/app/actions/remote/channel_bookmark.ts +++ /dev/null @@ -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}; - } -} diff --git a/app/actions/remote/file.ts b/app/actions/remote/file.ts index 0a4fefb98..14461f108 100644 --- a/app/actions/remote/file.ts +++ b/app/actions/remote/file.ts @@ -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}; diff --git a/app/actions/remote/search.ts b/app/actions/remote/search.ts index 42b81e799..86ab63a8c 100644 --- a/app/actions/remote/search.ts +++ b/app/actions/remote/search.ts @@ -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) { diff --git a/app/actions/websocket/event.ts b/app/actions/websocket/event.ts deleted file mode 100644 index 36b46b648..000000000 --- a/app/actions/websocket/event.ts +++ /dev/null @@ -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; - } -} diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 6588c8f57..2a8f1f297 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -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); diff --git a/app/client/rest/base.ts b/app/client/rest/base.ts index 383d74321..343cf8f3d 100644 --- a/app/client/rest/base.ts +++ b/app/client/rest/base.ts @@ -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`; } diff --git a/app/client/rest/channel_bookmark.ts b/app/client/rest/channel_bookmark.ts deleted file mode 100644 index 7f55e716d..000000000 --- a/app/client/rest/channel_bookmark.ts +++ /dev/null @@ -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; - updateChannelBookmark(channelId: string, bookmark: ChannelBookmark, connectionId?: string): Promise; - updateChannelBookmarkSortOrder(channelId: string, bookmarkId: string, newSortOrder: number, connectionId?: string): Promise; - deleteChannelBookmark(channelId: string, bookmarkId: string, connectionId?: string): Promise; - getChannelBookmarksForChannel(channelId: string, since: number): Promise; -} - -const ClientChannelBookmarks = >(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; diff --git a/app/client/rest/channels.ts b/app/client/rest/channels.ts index bf00a1440..80577f973 100644 --- a/app/client/rest/channels.ts +++ b/app/client/rest/channels.ts @@ -152,16 +152,14 @@ const ClientChannels = >(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 = >(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 = >(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'}, ); diff --git a/app/client/rest/files.ts b/app/client/rest/files.ts index 48c65f076..0c849c1de 100644 --- a/app/client/rest/files.ts +++ b/app/client/rest/files.ts @@ -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; searchFilesWithParams: (teamId: string, FileSearchParams: string) => Promise; @@ -59,19 +58,15 @@ const ClientFiles = >(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', diff --git a/app/client/rest/index.ts b/app/client/rest/index.ts index e023d8ca3..382afcbfd 100644 --- a/app/client/rest/index.ts +++ b/app/client/rest/index.ts @@ -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, diff --git a/app/client/websocket/index.ts b/app/client/websocket/index.ts index 05917bde3..54ed13c05 100644 --- a/app/client/websocket/index.ts +++ b/app/client/websocket/index.ts @@ -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; - } } diff --git a/app/components/button/index.tsx b/app/components/button/index.tsx index c64d7bc70..eb8bde1c3 100644 --- a/app/components/button/index.tsx +++ b/app/components/button/index.tsx @@ -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 ( {icon} diff --git a/app/components/channel_bookmarks/add_bookmark.tsx b/app/components/channel_bookmarks/add_bookmark.tsx deleted file mode 100644 index 2a71f4a68..000000000 --- a/app/components/channel_bookmarks/add_bookmark.tsx +++ /dev/null @@ -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 = () => ( - - ); - - 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 = ( - - - ); -}; - -export default BookmarkDocument; diff --git a/app/components/channel_bookmarks/channel_bookmark/bookmark_icon.tsx b/app/components/channel_bookmarks/channel_bookmark/bookmark_icon.tsx deleted file mode 100644 index 069112200..000000000 --- a/app/components/channel_bookmarks/channel_bookmark/bookmark_icon.tsx +++ /dev/null @@ -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; - file?: FileInfo | ExtractedFileInfo; - iconSize: number; - imageStyle?: StyleProp; - imageUrl?: string; - genericStyle: StyleProp; -} - -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 ( - - ); - } else if (imageUrl && !emoji && !hasImageError) { - return ( - - ); - } else if (emoji) { - return ( - - ); - } - - return ( - - ); -}; - -export default BookmarkIcon; diff --git a/app/components/channel_bookmarks/channel_bookmark/bookmark_options.tsx b/app/components/channel_bookmarks/channel_bookmark/bookmark_options.tsx deleted file mode 100644 index ae96992bb..000000000 --- a/app/components/channel_bookmarks/channel_bookmark/bookmark_options.tsx +++ /dev/null @@ -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>; -} - -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: ( - - ), - }, {}, 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 && ( - - - {bookmark.displayName} - - - )} - - {canEditBookmarks && - - } - {canCopyPublicLink && - - } - {canShare && - - } - {canDeleteBookmarks && - - } - - - ); -}; - -export default ChannelBookmarkOptions; diff --git a/app/components/channel_bookmarks/channel_bookmark/channel_bookmark.tsx b/app/components/channel_bookmarks/channel_bookmark/channel_bookmark.tsx deleted file mode 100644 index 933045274..000000000 --- a/app/components/channel_bookmarks/channel_bookmark/channel_bookmark.tsx +++ /dev/null @@ -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(); - const serverUrl = useServerUrl(); - const intl = useIntl(); - const {bottom} = useSafeAreaInsets(); - const [action, setAction] = useState('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 = () => ( - - ); - - 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 ( - - ); - } - - return ( - - ); -}; - -export default ChannelBookmark; diff --git a/app/components/channel_bookmarks/channel_bookmark/index.tsx b/app/components/channel_bookmarks/channel_bookmark/index.tsx deleted file mode 100644 index 81833f8d9..000000000 --- a/app/components/channel_bookmarks/channel_bookmark/index.tsx +++ /dev/null @@ -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)); diff --git a/app/components/channel_bookmarks/channel_bookmarks.tsx b/app/components/channel_bookmarks/channel_bookmarks.tsx deleted file mode 100644 index bfe53396f..000000000 --- a/app/components/channel_bookmarks/channel_bookmarks.tsx +++ /dev/null @@ -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) => { - return ( - - ); - }, [ - attachmentIndex, bookmarks, canDownloadFiles, canDeleteBookmarks, canEditBookmarks, - handlePreviewPress, publicLinkEnabled, - ]); - - const renderItemSeparator = useCallback(() => (), []); - - const onScrolled = useCallback((e: NativeSyntheticEvent) => { - 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 ( - - ); - } - - if (bookmarks.length) { - return ( - - - - - {canAddBookmarks && - - {allowEndFade && - - } - - - } - - {separator && - - } - - - ); - } - - return null; -}; - -export default ChannelBookmarks; diff --git a/app/components/channel_bookmarks/index.ts b/app/components/channel_bookmarks/index.ts deleted file mode 100644 index 1248da186..000000000 --- a/app/components/channel_bookmarks/index.ts +++ /dev/null @@ -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)); diff --git a/app/components/document/index.tsx b/app/components/document/index.tsx deleted file mode 100644 index a5cd6e143..000000000 --- a/app/components/document/index.tsx +++ /dev/null @@ -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(({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>(); - - 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; diff --git a/app/components/files/document_file.tsx b/app/components/files/document_file.tsx index dbf65bdb7..76ab3295f 100644 --- a/app/components/files/document_file.tsx +++ b/app/components/files/document_file.tsx @@ -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(({backgroundColor, canDownloadFiles, disabled = false, file}: DocumentFileProps, ref) => { +const DocumentFile = forwardRef(({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(null); + let client: Client | undefined; + try { + client = NetworkManager.getClient(serverUrl); + } catch { + // do nothing + } + const downloadTask = useRef>(); + + 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(({backgroundColo ); let fileAttachmentComponent = icon; - if (progress) { + if (downloading) { fileAttachmentComponent = ( <> {icon} @@ -63,19 +189,9 @@ const DocumentFile = forwardRef(({backgroundColo } return ( - - - {fileAttachmentComponent} - - + + {fileAttachmentComponent} + ); }); diff --git a/app/components/files/file.tsx b/app/components/files/file.tsx index 83f661b08..1041439fa 100644 --- a/app/components/files/file.tsx +++ b/app/components/files/file.tsx @@ -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(null); + const document = useRef(null); const theme = useTheme(); const style = getStyleSheet(theme); @@ -108,7 +101,6 @@ const File = ({ const renderCardWithImage = (fileIcon: JSX.Element) => { const fileInfo = ( + + {Boolean(nonVisibleImagesCount) && @@ -191,7 +177,6 @@ const File = ({ @@ -199,7 +184,6 @@ const File = ({ const fileInfo = ( { }; }); -const FileInfo = ({disabled, file, channelName, showDate, onPress}: FileInfoProps) => { +const FileInfo = ({file, channelName, showDate, onPress}: FileInfoProps) => { const theme = useTheme(); const style = getStyleSheet(theme); - return ( - + - {decodeURIComponent(file.name.trim())} + {file.name.trim()} {channelName && diff --git a/app/components/files/image_file.tsx b/app/components/files/image_file.tsx index 580ce076d..ac48ffd1f 100644 --- a/app/components/files/image_file.tsx +++ b/app/components/files/image_file.tsx @@ -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; }; diff --git a/app/components/floating_text_input_label/index.tsx b/app/components/floating_text_input_label/index.tsx index 0734a3160..708ac8b45 100644 --- a/app/components/floating_text_input_label/index.tsx +++ b/app/components/floating_text_input_label/index.tsx @@ -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, diff --git a/app/components/post_list/more_messages/more_messages.tsx b/app/components/post_list/more_messages/more_messages.tsx index 381d8516a..8b431a490 100644 --- a/app/components/post_list/more_messages/more_messages.tsx +++ b/app/components/post_list/more_messages/more_messages.tsx @@ -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)); diff --git a/app/constants/database.ts b/app/constants/database.ts index bf30b15ec..d302c280a 100644 --- a/app/constants/database.ts +++ b/app/constants/database.ts @@ -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 { diff --git a/app/constants/permissions.ts b/app/constants/permissions.ts index 22ff91433..1b8637957 100644 --- a/app/constants/permissions.ts +++ b/app/constants/permissions.ts @@ -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', }; diff --git a/app/constants/screens.ts b/app/constants/screens.ts index 7ae21922e..2c0014171 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -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([ @@ -172,7 +168,6 @@ export const SCREENS_WITH_TRANSPARENT_BACKGROUND = new Set([ PERMALINK, REVIEW_APP, SNACK_BAR, - GENERIC_OVERLAY, ]); export const SCREENS_AS_BOTTOM_SHEET = new Set([ diff --git a/app/constants/snack_bar.ts b/app/constants/snack_bar.ts index f36053ab0..57491b93f 100644 --- a/app/constants/snack_bar.ts +++ b/app/constants/snack_bar.ts @@ -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 = { 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', diff --git a/app/constants/view.ts b/app/constants/view.ts index d42ab6914..50ee021a0 100644 --- a/app/constants/view.ts +++ b/app/constants/view.ts @@ -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, diff --git a/app/constants/websocket.ts b/app/constants/websocket.ts index 987f9de65..51c745284 100644 --- a/app/constants/websocket.ts +++ b/app/constants/websocket.ts @@ -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; diff --git a/app/database/manager/__mocks__/index.ts b/app/database/manager/__mocks__/index.ts index 277454c08..0d38775cc 100644 --- a/app/database/manager/__mocks__/index.ts +++ b/app/database/manager/__mocks__/index.ts @@ -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, diff --git a/app/database/manager/index.ts b/app/database/manager/index.ts index 27b7b46e7..93eed0b39 100644 --- a/app/database/manager/index.ts +++ b/app/database/manager/index.ts @@ -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, diff --git a/app/database/migration/server/index.ts b/app/database/migration/server/index.ts index 73a2a34cd..6fc926695 100644 --- a/app/database/migration/server/index.ts +++ b/app/database/migration/server/index.ts @@ -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: [ diff --git a/app/database/models/server/channel.ts b/app/database/models/server/channel.ts index 420e19b5a..80c40fcfd 100644 --- a/app/database/models/server/channel.ts +++ b/app/database/models/server/channel.ts @@ -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; - /** bookmarks : All bookmarks for this channel */ - @children(CHANNEL_BOOKMARK) bookmarks!: Query; - /** posts : All posts made in that channel */ @children(POST) posts!: Query; diff --git a/app/database/models/server/channel_bookmark.ts b/app/database/models/server/channel_bookmark.ts deleted file mode 100644 index 084b23dae..000000000 --- a/app/database/models/server/channel_bookmark.ts +++ /dev/null @@ -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; - - /** creator : The USER who created this CHANNEL_BOOKMARK */ - @immutableRelation(USER, 'owner_id') owner!: Relation; - - /** file : The FILE attached to this CHANNEL_BOOKMARK */ - @immutableRelation(FILE, 'file_id') file!: Relation; - - 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; - }; -} diff --git a/app/database/models/server/file.ts b/app/database/models/server/file.ts index 96ba7fc2a..45c62e497 100644 --- a/app/database/models/server/file.ts +++ b/app/database/models/server/file.ts @@ -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'}, }; diff --git a/app/database/models/server/index.ts b/app/database/models/server/index.ts index 6ea0df906..8368b53da 100644 --- a/app/database/models/server/index.ts +++ b/app/database/models/server/index.ts @@ -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'; diff --git a/app/database/models/server/user.ts b/app/database/models/server/user.ts index 1edbaba54..c9c22154e 100644 --- a/app/database/models/server/user.ts +++ b/app/database/models/server/user.ts @@ -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'}, diff --git a/app/database/operator/base_data_operator/index.ts b/app/database/operator/base_data_operator/index.ts index 1b77b665f..aace2f2aa 100644 --- a/app/database/operator/base_data_operator/index.ts +++ b/app/database/operator/base_data_operator/index.ts @@ -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}>} + * @returns {Promise<{ProcessRecordResults}>} */ processRecords = async ({createOrUpdateRawValues = [], deleteRawValues = [], tableName, buildKeyRecordBy, fieldName, shouldUpdate}: ProcessRecordsArgs): Promise> => { 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, record) => { + const recordsByKeys = createOrUpdateRaws.reduce((result: Record, 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;} transformer - * @returns {Promise} + * @param {Model[]} prepareRecord.deleteRaws + * @param {(TransformerArgs) => Promise;} transformer + * @returns {Promise} */ prepareRecords = async ({tableName, createRaws, deleteRaws, updateRaws, transformer}: OperationArgs): Promise => { if (!this.database) { @@ -210,7 +210,7 @@ export default class BaseDataOperator { * @returns {Promise} */ async handleRecords({buildKeyRecordBy, fieldName, transformer, createOrUpdateRawValues, deleteRawValues = [], tableName, prepareRecordsOnly = true, shouldUpdate}: HandleRecordsArgs, description: string): Promise { - if (!createOrUpdateRawValues.length && !deleteRawValues.length) { + if (!createOrUpdateRawValues.length) { logWarning( `An empty "rawValues" array has been passed to the handleRecords method for tableName ${tableName}`, ); diff --git a/app/database/operator/server_data_operator/handlers/channel.test.ts b/app/database/operator/server_data_operator/handlers/channel.test.ts index eccb9fc47..d5c3b5df8 100644 --- a/app/database/operator/server_data_operator/handlers/channel.test.ts +++ b/app/database/operator/server_data_operator/handlers/channel.test.ts @@ -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 () => { diff --git a/app/database/operator/server_data_operator/handlers/channel.ts b/app/database/operator/server_data_operator/handlers/channel.ts index b5f7f3cbe..c5b750721 100644 --- a/app/database/operator/server_data_operator/handlers/channel.ts +++ b/app/database/operator/server_data_operator/handlers/channel.ts @@ -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; - handleChannelBookmark: ({bookmarks, prepareRecordsOnly}: HandleChannelBookmarkArgs) => Promise; handleChannelMembership: ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => Promise; handleMyChannelSettings: ({settings, prepareRecordsOnly}: HandleMyChannelSettingsArgs) => Promise; handleChannelInfo: ({channelInfos, prepareRecordsOnly}: HandleChannelInfoArgs) => Promise; - handleMyChannel: ({channels, myChannels, isCRTEnabled, prepareRecordsOnly}: HandleMyChannelArgs) => Promise; + handleMyChannel: ({channels, myChannels, isCRTEnabled, prepareRecordsOnly}: HandleMyChannelArgs) => Promise; } const ChannelHandler = >(superclass: TBase) => class extends superclass { @@ -221,7 +217,7 @@ const ChannelHandler = >(super * @param {boolean} myChannelsArgs.prepareRecordsOnly * @returns {Promise} */ - handleMyChannel = async ({channels, myChannels, isCRTEnabled, prepareRecordsOnly = true}: HandleMyChannelArgs): Promise => { + handleMyChannel = async ({channels, myChannels, isCRTEnabled, prepareRecordsOnly = true}: HandleMyChannelArgs): Promise => { if (!myChannels?.length) { logWarning( 'An empty or undefined "myChannels" array has been passed to the handleMyChannel method', @@ -239,6 +235,7 @@ const ChannelHandler = >(super } const isCRT = isCRTEnabled ?? await getIsCRTEnabled(this.database); + const channelMap = channels.reduce((result: Record, channel) => { result[channel.id] = channel; return result; @@ -356,87 +353,6 @@ const ChannelHandler = >(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} - */ - handleChannelBookmark = async ({bookmarks, prepareRecordsOnly}: HandleChannelBookmarkArgs): Promise => { - 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(CHANNEL_BOOKMARK).query( - Q.where('id', Q.oneOf(keys)), - ).fetch(); - const bookmarkMap = new Map(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; diff --git a/app/database/operator/server_data_operator/handlers/index.test.ts b/app/database/operator/server_data_operator/handlers/index.test.ts index 09b02c8a3..9ce3d4744 100644 --- a/app/database/operator/server_data_operator/handlers/index.test.ts +++ b/app/database/operator/server_data_operator/handlers/index.test.ts @@ -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); diff --git a/app/database/operator/server_data_operator/handlers/index.ts b/app/database/operator/server_data_operator/handlers/index.ts index 8da5fbd39..15ce6f282 100644 --- a/app/database/operator/server_data_operator/handlers/index.ts +++ b/app/database/operator/server_data_operator/handlers/index.ts @@ -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} - */ - handleFiles = async ({files, prepareRecordsOnly}: HandleFilesArgs): Promise => { - 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({ - createOrUpdateRawValues: raws.createOrUpdateFiles, - tableName: FILE, - fieldName: 'id', - deleteRawValues: raws.deleteFiles, - shouldUpdate: shouldUpdateFileRecord, - })); - - const preparedFiles = await this.prepareRecords({ - 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 diff --git a/app/database/operator/server_data_operator/handlers/post.ts b/app/database/operator/server_data_operator/handlers/post.ts index 56c28571a..2b0d92e3e 100644 --- a/app/database/operator/server_data_operator/handlers/post.ts +++ b/app/database/operator/server_data_operator/handlers/post.ts @@ -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 = >(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} + */ + handleFiles = async ({files, prepareRecordsOnly}: HandleFilesArgs): Promise => { + 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} postsMap diff --git a/app/database/operator/server_data_operator/transformers/channel.ts b/app/database/operator/server_data_operator/transformers/channel.ts index 671d0f42c..4c124c691 100644 --- a/app/database/operator/server_data_operator/transformers/channel.ts +++ b/app/database/operator/server_data_operator/transformers/channel.ts @@ -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; }; - -/** - * 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} - */ -export const transformChannelBookmarkRecord = ({action, database, value}: TransformerArgs): Promise => { - 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; -}; diff --git a/app/database/operator/server_data_operator/transformers/general.test.ts b/app/database/operator/server_data_operator/transformers/general.test.ts index 6121db4f4..12e25c6e1 100644 --- a/app/database/operator/server_data_operator/transformers/general.test.ts +++ b/app/database/operator/server_data_operator/transformers/general.test.ts @@ -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'); - }); -}); diff --git a/app/database/operator/server_data_operator/transformers/general.ts b/app/database/operator/server_data_operator/transformers/general.ts index 8264739b7..d15778076 100644 --- a/app/database/operator/server_data_operator/transformers/general.ts +++ b/app/database/operator/server_data_operator/transformers/general.ts @@ -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; }; - -/** - * 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} - */ -export const transformFileRecord = ({action, database, value}: TransformerArgs): Promise => { - 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; -}; diff --git a/app/database/operator/server_data_operator/transformers/post.test.ts b/app/database/operator/server_data_operator/transformers/post.test.ts index 6f018d2fb..53bea8c62 100644 --- a/app/database/operator/server_data_operator/transformers/post.test.ts +++ b/app/database/operator/server_data_operator/transformers/post.test.ts @@ -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); diff --git a/app/database/operator/server_data_operator/transformers/post.ts b/app/database/operator/server_data_operator/transformers/post.ts index 7bd63609d..0b42b8b6a 100644 --- a/app/database/operator/server_data_operator/transformers/post.ts +++ b/app/database/operator/server_data_operator/transformers/post.ts @@ -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; }; +/** + * 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} + */ +export const transformFileRecord = ({action, database, value}: TransformerArgs): Promise => { + 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; +}; + /** * transformDraftRecord: Prepares a record of the SERVER database 'Draft' table for update or create actions. * @param {TransformerArgs} operator diff --git a/app/database/schema/server/index.ts b/app/database/schema/server/index.ts index 7f0203c16..8220f092e 100644 --- a/app/database/schema/server/index.ts +++ b/app/database/schema/server/index.ts @@ -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, diff --git a/app/database/schema/server/table_schemas/channel_bookmark.ts b/app/database/schema/server/table_schemas/channel_bookmark.ts deleted file mode 100644 index c7d62188d..000000000 --- a/app/database/schema/server/table_schemas/channel_bookmark.ts +++ /dev/null @@ -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}, - ], -}); diff --git a/app/database/schema/server/table_schemas/index.ts b/app/database/schema/server/table_schemas/index.ts index ae6337034..27676546e 100644 --- a/app/database/schema/server/table_schemas/index.ts +++ b/app/database/schema/server/table_schemas/index.ts @@ -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'; diff --git a/app/database/schema/server/test.ts b/app/database/schema/server/test.ts index da7cc1fc6..e64685048 100644 --- a/app/database/schema/server/test.ts +++ b/app/database/schema/server/test.ts @@ -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, diff --git a/app/hooks/files.ts b/app/hooks/files.ts index 49da59e51..6daa81947 100644 --- a/app/hooks/files.ts +++ b/app/hooks/files.ts @@ -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([]); - - useEffect(() => { - getFileInfo(serverUrl, bookmarks, publicLinkEnabled, setFiles); - }, [serverUrl, bookmarks, publicLinkEnabled]); - - return files; -}; diff --git a/app/managers/websocket_manager.ts b/app/managers/websocket_manager.ts index 71870abf8..c86f5ddb9 100644 --- a/app/managers/websocket_manager.ts +++ b/app/managers/websocket_manager.ts @@ -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(() => { diff --git a/app/products/calls/components/floating_call_container.tsx b/app/products/calls/components/floating_call_container.tsx index 3ab1741f5..d5c264efa 100644 --- a/app/products/calls/components/floating_call_container.tsx +++ b/app/products/calls/components/floating_call_container.tsx @@ -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, diff --git a/app/products/calls/hooks.ts b/app/products/calls/hooks.ts index 49f954d0a..c319912a9 100644 --- a/app/products/calls/hooks.ts +++ b/app/products/calls/hooks.ts @@ -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); diff --git a/app/queries/app/global.ts b/app/queries/app/global.ts index 1c62afdcb..e1ff99bb8 100644 --- a/app/queries/app/global.ts +++ b/app/queries/app/global.ts @@ -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; - } -}; diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index fd846912e..71c9b2225 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -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; - -function prepareChannels( - operator: ServerDataOperator, - channels?: Channel[], - channelInfos?: ChannelInfo[], - channelMemberships?: ChannelMembershipsExtended[], - memberships?: ChannelMembership[], - isCRTEnabled?: boolean, -): Array> { - 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> { const channelInfos: ChannelInfo[] = []; const channelMap: Record = {}; @@ -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 => { @@ -173,27 +169,6 @@ export const prepareDeleteChannel = async (channel: ChannelModel): Promise { - 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; }; diff --git a/app/queries/servers/channel_bookmark.ts b/app/queries/servers/channel_bookmark.ts deleted file mode 100644 index 9baeeecde..000000000 --- a/app/queries/servers/channel_bookmark.ts +++ /dev/null @@ -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(CHANNEL_BOOKMARK).find(bookmarkId); - return bookmark; - } catch { - return undefined; - } -}; - -export const queryBookmarks = (database: Database, channelId: string) => { - return database.get(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']); -}; diff --git a/app/queries/servers/entry.ts b/app/queries/servers/entry.ts index 26888cf1c..d237035c6 100644 --- a/app/queries/servers/entry.ts +++ b/app/queries/servers/entry.ts @@ -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}`, []], ], }); }); diff --git a/app/queries/servers/file.ts b/app/queries/servers/file.ts index 501181954..7226058d2 100644 --- a/app/queries/servers/file.ts +++ b/app/queries/servers/file.ts @@ -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(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(FILE).query( Q.where('post_id', postId), diff --git a/app/screens/channel/channel.tsx b/app/screens/channel/channel.tsx index d9ad6fe15..f7f524df5 100644 --- a/app/screens/channel/channel.tsx +++ b/app/screens/channel/channel.tsx @@ -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 && } diff --git a/app/screens/channel/header/bookmarks.tsx b/app/screens/channel/header/bookmarks.tsx deleted file mode 100644 index 21a6da487..000000000 --- a/app/screens/channel/header/bookmarks.tsx +++ /dev/null @@ -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 ( - - - - - - - - - - - ); -}; - -export default ChannelHeaderBookmarks; diff --git a/app/screens/channel/header/header.tsx b/app/screens/channel/header/header.tsx index 27b584980..7d83a064a 100644 --- a/app/screens/channel/header/header.tsx +++ b/app/screens/channel/header/header.tsx @@ -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 = ({ - {isBookmarksEnabled && hasBookmarks && shouldRenderBookmarks && - - } ); }; diff --git a/app/screens/channel/header/index.ts b/app/screens/channel/header/index.ts index 64ee1767c..e59210e96 100644 --- a/app/screens/channel/header/index.ts +++ b/app/screens/channel/header/index.ts @@ -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, diff --git a/app/screens/channel/index.tsx b/app/screens/channel/index.tsx index 049452fde..094659b1b 100644 --- a/app/screens/channel/index.tsx +++ b/app/screens/channel/index.tsx @@ -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, }; }); diff --git a/app/screens/channel_bookmark/components/bookmark_detail.tsx b/app/screens/channel_bookmark/components/bookmark_detail.tsx deleted file mode 100644 index d6343a9bb..000000000 --- a/app/screens/channel_bookmark/components/bookmark_detail.tsx +++ /dev/null @@ -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 ( - - - - - - - - ); -}; - -export default BookmarkDetail; diff --git a/app/screens/channel_bookmark/components/bookmark_file/bookmark_file.tsx b/app/screens/channel_bookmark/components/bookmark_file/bookmark_file.tsx deleted file mode 100644 index 634ddf22e..000000000 --- a/app/screens/channel_bookmark/components/bookmark_file/bookmark_file.tsx +++ /dev/null @@ -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(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 = ( - - {error} - - ); - } else if (uploading) { - info = ( - - ); - } else if (file) { - info = ( - - {`${file.extension} ${getFormattedFileSize(file.size || 0)}`} - - ); - } - - if (file) { - return ( - - - - - - - - {decodeURIComponent(file.name.trim())} - - {info} - - {failed && - - } - - - - - - - - {uploading && - - - - } - - ); - } - - return null; -}; - -export default BookmarkFile; diff --git a/app/screens/channel_bookmark/components/bookmark_file/index.ts b/app/screens/channel_bookmark/components/bookmark_file/index.ts deleted file mode 100644 index 723083e70..000000000 --- a/app/screens/channel_bookmark/components/bookmark_file/index.ts +++ /dev/null @@ -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)); diff --git a/app/screens/channel_bookmark/components/bookmark_link.tsx b/app/screens/channel_bookmark/components/bookmark_link.tsx deleted file mode 100644 index a4d59ca47..000000000 --- a/app/screens/channel_bookmark/components/bookmark_link.tsx +++ /dev/null @@ -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 ( - - - } - /> - - - - - ); -}; - -export default BookmarkLink; diff --git a/app/screens/channel_bookmark/index.tsx b/app/screens/channel_bookmark/index.tsx deleted file mode 100644 index 81f310494..000000000 --- a/app/screens/channel_bookmark/index.tsx +++ /dev/null @@ -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(original); - const [file, setFile] = useState(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 ( - - {type === 'link' && - - } - {type === 'file' && - - } - {Boolean(bookmark) && - <> - - {canDeleteBookmarks && - -