From e67ac47ce80960dbcd35f7674ddb9a0386350225 Mon Sep 17 00:00:00 2001 From: ThrRip Date: Thu, 7 Dec 2023 03:43:14 +0000 Subject: [PATCH 01/47] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1107 of 1107 strings) Translation: Mattermost/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/zh_Hans/ --- assets/base/i18n/zh-CN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/base/i18n/zh-CN.json b/assets/base/i18n/zh-CN.json index 36a5ffbbb..b1126d781 100644 --- a/assets/base/i18n/zh-CN.json +++ b/assets/base/i18n/zh-CN.json @@ -1,6 +1,6 @@ { "about.date": "编译日期:", - "about.enterpriseEditionLearn": "了解更多关于企业版 ", + "about.enterpriseEditionLearn": "深入了解企业版: ", "about.enterpriseEditionSt": "位于防火墙后的现代通讯方式。", "about.enterpriseEditione1": "企业版", "about.hash": "编译哈希:", @@ -74,7 +74,7 @@ "apps.error.responses.unexpected_error": "收到未预料的错误。", "apps.error.responses.unexpected_type": "应用响应类型不是预期的。响应类型:{type}", "apps.error.responses.unknown_field_error": "收到未知字段的错误。字段名称:`{field}`。错误:`{error}`。", - "apps.error.responses.unknown_type": "不支援应用响应的类型。响应类型:{type}。", + "apps.error.responses.unknown_type": "不支持此应用的响应类型。响应类型:{type}。", "apps.error.unknown": "发生未知错误。", "apps.suggestion.dynamic.error": "动态选择错误", "apps.suggestion.errors.parser_error": "解析错误", From 15fb6363aa7346970bb0cc105d106f1de0cb2256 Mon Sep 17 00:00:00 2001 From: Tanmay Thole <72058456+tanmaythole@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:20:18 +0530 Subject: [PATCH 02/47] fix: height for multiple line floating text input (#7688) --- app/components/floating_text_input_label/index.tsx | 12 ++++++++---- .../notification_auto_responder.tsx | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/components/floating_text_input_label/index.tsx b/app/components/floating_text_input_label/index.tsx index 74a8bdbb2..708ac8b45 100644 --- a/app/components/floating_text_input_label/index.tsx +++ b/app/components/floating_text_input_label/index.tsx @@ -95,6 +95,7 @@ type FloatingTextInputProps = TextInputProps & { label: string; labelTextStyle?: TextStyle; multiline?: boolean; + multilineInputHeight?: number; onBlur?: (event: NativeSyntheticEvent) => void; onFocus?: (e: NativeSyntheticEvent) => void; onLayout?: (e: LayoutChangeEvent) => void; @@ -117,6 +118,7 @@ const FloatingTextInput = forwardRef { const res: StyleProp = [styles.textInput, styles.input, textInputStyle]; if (multiline) { - res.push({height: 80, textAlignVertical: 'top'}); + const height = multilineInputHeight ? multilineInputHeight - 20 : 80; + res.push({height, textAlignVertical: 'top'}); } return res; - }, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline, editable]); + }, [styles, theme, shouldShowError, focused, textInputStyle, focusedLabel, multiline, multilineInputHeight, editable]); const textAnimatedTextStyle = useAnimatedStyle(() => { const inputText = placeholder || value || props.defaultValue; diff --git a/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx b/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx index eda42012b..92fcf4038 100644 --- a/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx +++ b/app/screens/settings/notification_auto_responder/notification_auto_responder.tsx @@ -117,6 +117,7 @@ const NotificationAutoResponder = ({currentUser, componentId}: NotificationAutoR keyboardAppearance={getKeyboardAppearanceFromTheme(theme)} label={intl.formatMessage(label)} multiline={true} + multilineInputHeight={154} onChangeText={setAutoResponderMessage} placeholder={intl.formatMessage(label)} placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.4)} From b405b3a872e3ab2fb25db5f87a8da49a29b1a73d Mon Sep 17 00:00:00 2001 From: "unified-ci-app[bot]" <121569378+unified-ci-app[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 13:16:45 +0100 Subject: [PATCH 03/47] Bump app build number to 500 (#7717) Co-authored-by: runner --- android/app/build.gradle | 2 +- fastlane/Gemfile.lock | 1 + ios/Mattermost.xcodeproj/project.pbxproj | 8 ++++---- ios/Mattermost/Info.plist | 2 +- ios/MattermostShare/Info.plist | 2 +- ios/NotificationService/Info.plist | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ca647e998..72f48f1e2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -111,7 +111,7 @@ android { applicationId "com.mattermost.rnbeta" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 499 + versionCode 500 versionName "2.12.0" testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' diff --git a/fastlane/Gemfile.lock b/fastlane/Gemfile.lock index 1c2669abd..acc6c9c79 100644 --- a/fastlane/Gemfile.lock +++ b/fastlane/Gemfile.lock @@ -216,6 +216,7 @@ GEM PLATFORMS arm64-darwin-22 arm64-darwin-23 + x86_64-darwin-20 x86_64-darwin-23 DEPENDENCIES diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index f6c9cf26d..27b17cd17 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -1931,7 +1931,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 499; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -1975,7 +1975,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 499; + CURRENT_PROJECT_VERSION = 500; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( @@ -2118,7 +2118,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 499; + CURRENT_PROJECT_VERSION = 500; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -2167,7 +2167,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 499; + CURRENT_PROJECT_VERSION = 500; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = UQ8HT4Q2XM; GCC_C_LANGUAGE_STANDARD = gnu11; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 3fbd5414f..cfc4896a3 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -37,7 +37,7 @@ CFBundleVersion - 499 + 500 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index b2a42d8b5..fa5364330 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 2.12.0 CFBundleVersion - 499 + 500 UIAppFonts OpenSans-Bold.ttf diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index 9dfb10119..c5196d588 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 2.12.0 CFBundleVersion - 499 + 500 NSExtension NSExtensionPointIdentifier From ff8ebb04a3cbf07dc3bfbb920faa2b0cb65c9dcc Mon Sep 17 00:00:00 2001 From: Mario Vitale Date: Fri, 15 Dec 2023 13:34:59 +0100 Subject: [PATCH 04/47] Fix typo: use correct base branch in PR creation (#7720) --- scripts/bump.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bump.sh b/scripts/bump.sh index 19c695b04..386a17159 100755 --- a/scripts/bump.sh +++ b/scripts/bump.sh @@ -87,7 +87,7 @@ if [ -n "${CREATE_PR}" ]; then PR_TITLE="Bump app ${BUMP_BUILD_NUMBER:+build}${BUMP_VERSION_NUMBER:+${BUMP_BUILD_NUMBER:+ and }version} number" gh pr create \ --repo mattermost/mattermost-mobile \ - --base main \ + --base "${BRANCH_TO_BUILD}" \ --head "${GIT_LOCAL_BRANCH}" \ --reviewer "${PR_REVIEWERS}" \ --title "$PR_TITLE" \ From 5ee400c74f09b730fa6d15dc1770cbc778547717 Mon Sep 17 00:00:00 2001 From: JaeMin Kim Date: Sun, 17 Dec 2023 03:55:20 +0000 Subject: [PATCH 05/47] Translated using Weblate (Korean) Currently translated at 98.3% (1089 of 1107 strings) Translation: Mattermost/mattermost-mobile-v2 Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile-v2/ko/ --- assets/base/i18n/ko.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/base/i18n/ko.json b/assets/base/i18n/ko.json index cd17a44aa..87d4e823f 100644 --- a/assets/base/i18n/ko.json +++ b/assets/base/i18n/ko.json @@ -119,6 +119,8 @@ "channel_info.close_gm": "그룹 메시지 닫기", "channel_info.close_gm_channel": "이 그룹 메시지를 닫으시겠어요? 홈 화면에서 채널에 제거되지만 언제든지 다시 열어볼 수 있습니다.", "channel_info.convert_failed": "{displayName} 채널을 비공개 채널로 변환하지 못했습니다.", + "channel_info.convert_gm_to_channel.team_selector_list.title": "팀 선택", + "channel_info.convert_gm_to_channel.warning.body.yourself": "당신", "channel_info.convert_private": "비공개 채널로 변환", "channel_info.convert_private_description": "{displayName} 채널을 비공개 채널로 변환할 때, 기록과 권한은 유지됩니다. 링크를 통해 공개적으로 공유된 파일들은 계속 접근할 수 있습니다. 비공개 채널의 접근은 초대를 통해서만 가능합니다.\n\n이 변경은 영구적이며 되돌릴 수 없습니다.\n\n{displayName} 채널을 비공개 채널로 변환하시겠어요?", "channel_info.convert_private_success": "{displayName} 채널은 이제 비공개 채널입니다.", From 025aefb162305cbdb02c307f3698b51684cc9ae0 Mon Sep 17 00:00:00 2001 From: Antonis Stamatiou Date: Mon, 18 Dec 2023 09:26:32 +0200 Subject: [PATCH 06/47] feat: Enable true labeled trigger and deprecate mattermod (#7718) --- .github/workflows/build-pr.yml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 8f6997109..6364a3e74 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -1,11 +1,9 @@ --- name: build-pr on: - push: - branches: - - build-pr-* - - build-pr-android-* - - build-pr-ios-* + pull_request: + types: + - labeled env: NODE_VERSION: 18.7.0 @@ -14,20 +12,25 @@ env: jobs: test: runs-on: ubuntu-22.04 + if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' || github.event.label.name == 'Build App for Android' }} steps: - name: ci/checkout-repo uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: ci/test uses: ./.github/actions/test build-ios-pr: runs-on: macos-12 - if: ${{ !contains(github.ref_name, 'android') }} + if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' }} needs: - test steps: - name: ci/checkout-repo uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: ci/prepare-ios-build uses: ./.github/actions/prepare-ios-build @@ -43,7 +46,7 @@ jobs: - name: ci/build-ios-pr env: - BRANCH_TO_BUILD: "${{ github.ref_name }}" + BRANCH_TO_BUILD: "${{ github.event.pull_request.head.ref }}" AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_PR_AWS_ACCESS_KEY_ID }}" AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_PR_AWS_SECRET_ACCESS_KEY }}" FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}" @@ -64,12 +67,14 @@ jobs: build-android-pr: runs-on: ubuntu-22.04 - if: ${{ !contains(github.ref_name, 'ios') }} + if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for Android' }} needs: - test steps: - name: ci/checkout-repo uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: ci/prepare-android-build uses: ./.github/actions/prepare-android-build @@ -81,7 +86,7 @@ jobs: - name: ci/build-android-pr env: - BRANCH_TO_BUILD: "${{ github.ref_name }}" + BRANCH_TO_BUILD: "${{ github.event.pull_request.head.ref }}" AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_PR_AWS_ACCESS_KEY_ID }}" AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_PR_AWS_SECRET_ACCESS_KEY }}" MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_PR_MATTERMOST_WEBHOOK_URL }}" From 2c1f31886800f604a64d8382aeb1ce23b6e0fb73 Mon Sep 17 00:00:00 2001 From: Christopher Poile Date: Thu, 21 Dec 2023 16:20:21 -0500 Subject: [PATCH 07/47] MM-54866 - Calls: Transcription support (#7703) * captions on videos from posts and searches * add the patch for react-native-video which fixes subtitle downloading * improve spacing * fix patch file * upgrade compass-icons; use cc icon * revert patch overwrite * fix patch * use useMemo * fix hitslops on pressables * use new Caption format; refactor for clarity * simplify tracks creation and use --- app/actions/remote/search.ts | 24 ++++- app/components/files/files.tsx | 5 +- app/components/files/index.ts | 1 + app/products/calls/context.ts | 6 ++ app/products/calls/types/calls.ts | 12 +++ app/products/calls/utils.ts | 36 +++++++- app/screens/gallery/footer/actions/action.tsx | 22 ++++- app/screens/gallery/footer/actions/index.tsx | 23 +++-- .../footer/actions/inverted_action.tsx | 73 +++++++++++++++ app/screens/gallery/footer/footer.tsx | 7 ++ app/screens/gallery/index.tsx | 28 +++++- app/screens/gallery/video_renderer/index.tsx | 17 +++- app/utils/gallery/index.ts | 3 +- package-lock.json | 12 +-- package.json | 4 +- patches/react-native-video+5.2.1.patch | 88 +++++++++++++++++++ types/api/files.d.ts | 1 + types/screens/gallery.ts | 4 + 18 files changed, 335 insertions(+), 31 deletions(-) create mode 100644 app/products/calls/context.ts create mode 100644 app/screens/gallery/footer/actions/inverted_action.tsx diff --git a/app/actions/remote/search.ts b/app/actions/remote/search.ts index af41b7e09..86ab63a8c 100644 --- a/app/actions/remote/search.ts +++ b/app/actions/remote/search.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {getPosts} from '@actions/local/post'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; @@ -17,6 +18,7 @@ import {fetchPostAuthors, fetchMissingChannelsFromPosts} from './post'; import {forceLogoutIfNecessary} from './session'; import type Model from '@nozbe/watermelondb/Model'; +import type PostModel from '@typings/database/models/servers/post'; export async function fetchRecentMentions(serverUrl: string): Promise { try { @@ -132,13 +134,27 @@ export const searchFiles = async (serverUrl: string, teamId: string, params: Fil const client = NetworkManager.getClient(serverUrl); const result = await client.searchFiles(teamId, params.terms); const files = result?.file_infos ? Object.values(result.file_infos) : []; - const allChannelIds = files.reduce((acc, f) => { + const [allChannelIds, allPostIds] = files.reduce<[Set, Set]>((acc, f) => { if (f.channel_id) { - acc.push(f.channel_id); + acc[0].add(f.channel_id); + } + if (f.post_id) { + acc[1].add(f.post_id); } return acc; - }, []); - const channels = [...new Set(allChannelIds)]; + }, [new Set(), new Set()]); + const channels = Array.from(allChannelIds.values()); + + // Attach the file's post's props to the FileInfo (needed for captioning videos) + const postIds = Array.from(allPostIds); + const posts = await getPosts(serverUrl, postIds); + const idToPost = posts.reduce>((acc, p) => { + acc[p.id] = p; + return acc; + }, {}); + files.forEach((f) => { + f.postProps = idToPost[f.post_id]?.props; + }); return {files, channels}; } catch (error) { logDebug('error on searchFiles', getFullErrorMessage(error)); diff --git a/app/components/files/files.tsx b/app/components/files/files.tsx index 3747fea43..da11d676f 100644 --- a/app/components/files/files.tsx +++ b/app/components/files/files.tsx @@ -24,6 +24,7 @@ type FilesProps = { location: string; isReplyPost: boolean; postId: string; + postProps: Record; publicLinkEnabled: boolean; } @@ -48,7 +49,7 @@ const styles = StyleSheet.create({ }, }); -const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, location, postId, publicLinkEnabled}: FilesProps) => { +const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, location, postId, postProps, publicLinkEnabled}: FilesProps) => { const galleryIdentifier = `${postId}-fileAttachments-${location}`; const [inViewPort, setInViewPort] = useState(false); const isTablet = useIsTablet(); @@ -63,7 +64,7 @@ const Files = ({canDownloadFiles, failed, filesInfo, isReplyPost, layoutWidth, l }; const handlePreviewPress = preventDoubleTap((idx: number) => { - const items = filesForGallery.value.map((f) => fileToGalleryItem(f, f.user_id)); + const items = filesForGallery.value.map((f) => fileToGalleryItem(f, f.user_id, postProps)); openGalleryAtIndex(galleryIdentifier, idx, items); }); diff --git a/app/components/files/index.ts b/app/components/files/index.ts index 339fa7ad1..f2b7b15b7 100644 --- a/app/components/files/index.ts +++ b/app/components/files/index.ts @@ -45,6 +45,7 @@ const enhance = withObservables(['post'], ({database, post}: EnhanceProps) => { return { canDownloadFiles: observeCanDownloadFiles(database), postId: of$(post.id), + postProps: of$(post.props), publicLinkEnabled, filesInfo, }; diff --git a/app/products/calls/context.ts b/app/products/calls/context.ts new file mode 100644 index 000000000..3453e1ed5 --- /dev/null +++ b/app/products/calls/context.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {createContext} from 'react'; + +export const CaptionsEnabledContext = createContext([]); diff --git a/app/products/calls/types/calls.ts b/app/products/calls/types/calls.ts index c2bd051d0..3d0caa06e 100644 --- a/app/products/calls/types/calls.ts +++ b/app/products/calls/types/calls.ts @@ -193,3 +193,15 @@ export type CallsVersion = { version?: string; build?: string; }; + +export type SubtitleTrack = { + title?: string | undefined; + language?: string | undefined; + type: 'application/x-subrip' | 'application/ttml+xml' | 'text/vtt'; + uri: string; +}; + +export type SelectedSubtitleTrack = { + type: 'system' | 'disabled' | 'title' | 'language' | 'index'; + value?: string | number | undefined; +}; diff --git a/app/products/calls/utils.ts b/app/products/calls/utils.ts index bcfd80a77..04d554b0a 100644 --- a/app/products/calls/utils.ts +++ b/app/products/calls/utils.ts @@ -3,14 +3,16 @@ import {makeCallsBaseAndBadgeRGB, rgbToCSS} from '@mattermost/calls'; import {Alert} from 'react-native'; +import {TextTrackType} from 'react-native-video'; +import {buildFileUrl} from '@actions/remote/file'; import {Calls, Post} from '@constants'; import {NOTIFICATION_SUB_TYPE} from '@constants/push_notification'; import {isMinimumServerVersion} from '@utils/helpers'; import {displayUsername} from '@utils/user'; -import type {CallSession, CallsTheme, CallsVersion} from '@calls/types/calls'; -import type {CallsConfig} from '@mattermost/calls/lib/types'; +import type {CallSession, CallsTheme, CallsVersion, SelectedSubtitleTrack, SubtitleTrack} from '@calls/types/calls'; +import type {CallsConfig, Caption} from '@mattermost/calls/lib/types'; import type PostModel from '@typings/database/models/servers/post'; import type UserModel from '@typings/database/models/servers/user'; import type {IntlShape} from 'react-intl'; @@ -201,3 +203,33 @@ export function isCallsStartedMessage(payload?: NotificationData) { // calls will be >= 0.21.0, and push proxy will be >= 5.27.0 return (payload?.message === 'You\'ve been invited to a call' || callsMessageRegex.test(payload?.message || '')); } + +export const hasCaptions = (postProps?: Record & { captions?: Caption[] }): boolean => { + return !(!postProps || !postProps.captions?.[0]); +}; + +export const getTranscriptionUri = (serverUrl: string, postProps?: Record & { captions?: Caption[] }): { + tracks?: SubtitleTrack[]; + selected: SelectedSubtitleTrack; +} => { + // Note: We're not using hasCaptions above because this tells typescript that the caption exists later. + // We could use some fancy typescript to do the same, but it's not worth the complexity. + if (!postProps || !postProps.captions?.[0]) { + return { + tracks: undefined, + selected: {type: 'disabled'}, + }; + } + + const tracks: SubtitleTrack[] = postProps.captions.map((t) => ({ + title: t.title, + language: t.language, + type: TextTrackType.VTT, + uri: buildFileUrl(serverUrl, t.file_id), + })); + + return { + tracks, + selected: {type: 'index', value: 0}, + }; +}; diff --git a/app/screens/gallery/footer/actions/action.tsx b/app/screens/gallery/footer/actions/action.tsx index 3cd74903c..4c6fea7b5 100644 --- a/app/screens/gallery/footer/actions/action.tsx +++ b/app/screens/gallery/footer/actions/action.tsx @@ -5,7 +5,15 @@ // See LICENSE.txt for license information. import React, {useCallback} from 'react'; -import {Platform, Pressable, type PressableAndroidRippleConfig, type PressableStateCallbackType, type StyleProp, type ViewStyle} from 'react-native'; +import { + Platform, + Pressable, + type PressableAndroidRippleConfig, + type PressableStateCallbackType, + type StyleProp, + StyleSheet, + type ViewStyle, +} from 'react-native'; import CompassIcon from '@components/compass_icon'; import {changeOpacity} from '@utils/theme'; @@ -26,11 +34,21 @@ const pressedStyle = ({pressed}: PressableStateCallbackType) => { return [{opacity}]; }; +const baseStyle = StyleSheet.create({ + container: { + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, +}); + const androidRippleConfig: PressableAndroidRippleConfig = {borderless: true, radius: 24, color: '#FFF'}; const Action = ({disabled, iconName, onPress, style}: Props) => { const pressableStyle = useCallback((pressed: PressableStateCallbackType) => ([ pressedStyle(pressed), + baseStyle.container, style, ]), [style]); @@ -38,7 +56,7 @@ const Action = ({disabled, iconName, onPress, style}: Props) => { diff --git a/app/screens/gallery/footer/actions/index.tsx b/app/screens/gallery/footer/actions/index.tsx index 7c0bf3a5c..6b86d2f8b 100644 --- a/app/screens/gallery/footer/actions/index.tsx +++ b/app/screens/gallery/footer/actions/index.tsx @@ -5,6 +5,8 @@ import {useManagedConfig} from '@mattermost/react-native-emm'; import React from 'react'; import {StyleSheet, View} from 'react-native'; +import InvertedAction from '@screens/gallery/footer/actions/inverted_action'; + import Action from './action'; type Props = { @@ -15,20 +17,22 @@ type Props = { onCopyPublicLink: () => void; onDownload: () => void; onShare: () => void; + hasCaptions: boolean; + captionEnabled: boolean; + onCaptionsPress: () => void; } const styles = StyleSheet.create({ container: { flexDirection: 'row', - }, - action: { - marginLeft: 24, + alignItems: 'center', + gap: 8, }, }); const Actions = ({ - canDownloadFiles, disabled, enablePublicLinks, fileId, - onCopyPublicLink, onDownload, onShare, + canDownloadFiles, disabled, enablePublicLinks, fileId, onCopyPublicLink, + onDownload, onShare, hasCaptions, captionEnabled, onCaptionsPress, }: Props) => { const managedConfig = useManagedConfig(); const canCopyPublicLink = !fileId.startsWith('uid') && enablePublicLinks && managedConfig.copyAndPasteProtection !== 'true'; @@ -41,19 +45,24 @@ const Actions = ({ iconName='link-variant' onPress={onCopyPublicLink} />} + {hasCaptions && + + } {canDownloadFiles && <> } diff --git a/app/screens/gallery/footer/actions/inverted_action.tsx b/app/screens/gallery/footer/actions/inverted_action.tsx new file mode 100644 index 000000000..0e25051dc --- /dev/null +++ b/app/screens/gallery/footer/actions/inverted_action.tsx @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import { + Platform, + Pressable, + type PressableAndroidRippleConfig, + type PressableStateCallbackType, + type StyleProp, + StyleSheet, + type ViewStyle, +} from 'react-native'; + +import CompassIcon from '@components/compass_icon'; + +type Props = { + activated: boolean; + iconName: string; + onPress: () => void; + style?: StyleProp; +} + +const pressedStyle = ({pressed}: PressableStateCallbackType) => { + let opacity = 1; + if (Platform.OS === 'ios' && pressed) { + opacity = 0.5; + } + + return [{opacity}]; +}; + +const baseStyle = StyleSheet.create({ + container: { + width: 40, + height: 40, + borderRadius: 4, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#000', + }, + containerActivated: { + backgroundColor: '#fff', + }, +}); + +const androidRippleConfig: PressableAndroidRippleConfig = {borderless: true, radius: 24, color: '#FFF'}; + +const InvertedAction = ({activated, iconName, onPress, style}: Props) => { + const pressableStyle = useCallback((pressed: PressableStateCallbackType) => ([ + pressedStyle(pressed), + baseStyle.container, + activated && baseStyle.containerActivated, + style, + ]), [style, activated]); + + return ( + + + + ); +}; + +export default InvertedAction; diff --git a/app/screens/gallery/footer/footer.tsx b/app/screens/gallery/footer/footer.tsx index 310805fb5..09ce90dbc 100644 --- a/app/screens/gallery/footer/footer.tsx +++ b/app/screens/gallery/footer/footer.tsx @@ -35,6 +35,9 @@ type Props = { post?: PostModel; style: StyleProp; teammateNameDisplay: string; + hasCaptions: boolean; + captionEnabled: boolean; + onCaptionsPress: () => void; } const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView); @@ -58,6 +61,7 @@ const Footer = ({ author, canDownloadFiles, channelName, currentUserId, enablePostIconOverride, enablePostUsernameOverride, enablePublicLink, hideActions, isDirectChannel, item, post, style, teammateNameDisplay, + hasCaptions, captionEnabled, onCaptionsPress, }: Props) => { const showActions = !hideActions && Boolean(item.id) && !item.id?.startsWith('uid'); const [action, setAction] = useState('none'); @@ -142,6 +146,9 @@ const Footer = ({ onCopyPublicLink={handleCopyLink} onDownload={handleDownload} onShare={handleShare} + hasCaptions={hasCaptions} + captionEnabled={captionEnabled} + onCaptionsPress={onCaptionsPress} /> } diff --git a/app/screens/gallery/index.tsx b/app/screens/gallery/index.tsx index 40d33290f..9a615e0dc 100644 --- a/app/screens/gallery/index.tsx +++ b/app/screens/gallery/index.tsx @@ -1,9 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {NativeModules, useWindowDimensions, Platform} from 'react-native'; +import {CaptionsEnabledContext} from '@calls/context'; +import {hasCaptions} from '@calls/utils'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import {useIsTablet} from '@hooks/device'; import {useGalleryControls} from '@hooks/gallery'; @@ -29,10 +31,27 @@ const GalleryScreen = ({componentId, galleryIdentifier, hideActions, initialInde const dim = useWindowDimensions(); const isTablet = useIsTablet(); const [localIndex, setLocalIndex] = useState(initialIndex); + const [captionsEnabled, setCaptionsEnabled] = useState(new Array(items.length).fill(true)); + const [captionsAvailable, setCaptionsAvailable] = useState([]); const {setControlsHidden, headerStyles, footerStyles} = useGalleryControls(); const dimensions = useMemo(() => ({width: dim.width, height: dim.height}), [dim.width]); const galleryRef = useRef(null); + useEffect(() => { + const captions = items.reduce((acc, item) => { + acc.push(hasCaptions(item.postProps)); + return acc; + }, [] as boolean[]); + setCaptionsAvailable(captions); + }, [items]); + + const onCaptionsPressIdx = useCallback((idx: number) => { + const enabled = [...captionsEnabled]; + enabled[idx] = !enabled[idx]; + setCaptionsEnabled(enabled); + }, [captionsEnabled, setCaptionsEnabled]); + const onCaptionsPress = useCallback(() => onCaptionsPressIdx(localIndex), [localIndex, onCaptionsPressIdx]); + const onClose = useCallback(() => { // We keep the un freeze here as we want // the screen to be visible when the gallery @@ -63,7 +82,7 @@ const GalleryScreen = ({componentId, galleryIdentifier, hideActions, initialInde useAndroidHardwareBackHandler(componentId, close); return ( - <> +
- + ); }; diff --git a/app/screens/gallery/video_renderer/index.tsx b/app/screens/gallery/video_renderer/index.tsx index f61ed5966..009d83f48 100644 --- a/app/screens/gallery/video_renderer/index.tsx +++ b/app/screens/gallery/video_renderer/index.tsx @@ -1,13 +1,22 @@ // 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 React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter, Platform, StyleSheet, useWindowDimensions} from 'react-native'; -import Animated, {Easing, useAnimatedRef, useAnimatedStyle, useSharedValue, withTiming, type WithTimingConfig} from 'react-native-reanimated'; +import Animated, { + Easing, + useAnimatedRef, + useAnimatedStyle, + useSharedValue, + withTiming, + type WithTimingConfig, +} from 'react-native-reanimated'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import Video, {type OnPlaybackRateData} from 'react-native-video'; import {updateLocalFilePath} from '@actions/local/file'; +import {CaptionsEnabledContext} from '@calls/context'; +import {getTranscriptionUri} from '@calls/utils'; import CompassIcon from '@components/compass_icon'; import {Events} from '@constants'; import {GALLERY_FOOTER_HEIGHT, VIDEO_INSET} from '@constants/gallery'; @@ -59,12 +68,14 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul const serverUrl = useServerUrl(); const videoRef = useAnimatedRef