diff --git a/.gitignore b/.gitignore index 2fa17c24c..578a65aac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ mattermost.keystore tmp/ .env env.d.ts +*.apk +*.aab +*.ipa */**/compass-icons.ttf @@ -30,8 +33,6 @@ xcuserdata *.moved-aside DerivedData *.hmap -*.ipa -*.apk *.xcuserstate project.xcworkspace ios/Pods diff --git a/app/actions/remote/preference.ts b/app/actions/remote/preference.ts index 8a73a6cd7..6f1f4a650 100644 --- a/app/actions/remote/preference.ts +++ b/app/actions/remote/preference.ts @@ -1,12 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {General, Preferences} from '@constants'; +import {DeviceEventEmitter} from 'react-native'; + +import {handleReconnect} from '@actions/websocket'; +import {Events, General, Preferences} from '@constants'; import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; import {getChannelById} from '@queries/servers/channel'; +import {truncateCrtRelatedTables} from '@queries/servers/entry'; import {querySavedPostsPreferences} from '@queries/servers/preference'; import {getCurrentUserId} from '@queries/servers/system'; +import EphemeralStore from '@store/ephemeral_store'; import {getUserIdFromChannelName} from '@utils/user'; import {forceLogoutIfNecessary} from './session'; @@ -185,3 +190,11 @@ export const savePreferredSkinTone = async (serverUrl: string, skinCode: string) return {error}; } }; + +export const handleCRTToggled = async (serverUrl: string) => { + const currentServerUrl = await DatabaseManager.getActiveServerUrl(); + await truncateCrtRelatedTables(serverUrl); + await handleReconnect(serverUrl); + EphemeralStore.setEnablingCRT(false); + DeviceEventEmitter.emit(Events.CRT_TOGGLED, serverUrl === currentServerUrl); +}; diff --git a/app/actions/websocket/preferences.ts b/app/actions/websocket/preferences.ts index a0173d59c..e19991e3a 100644 --- a/app/actions/websocket/preferences.ts +++ b/app/actions/websocket/preferences.ts @@ -1,48 +1,32 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {DeviceEventEmitter} from 'react-native'; - import {updateDmGmDisplayName} from '@actions/local/channel'; -import {appEntry} from '@actions/remote/entry'; import {fetchPostById} from '@actions/remote/post'; -import {Events, Preferences} from '@constants'; +import {handleCRTToggled} from '@actions/remote/preference'; +import {Preferences} from '@constants'; import DatabaseManager from '@database/manager'; -import {truncateCrtRelatedTables} from '@queries/servers/entry'; import {getPostById} from '@queries/servers/post'; import {deletePreferences, differsFromLocalNameFormat, getHasCRTChanged} from '@queries/servers/preference'; - -async function handleCRTToggled(serverUrl: string) { - const currentServerUrl = await DatabaseManager.getActiveServerUrl(); - await truncateCrtRelatedTables(serverUrl); - appEntry(serverUrl); - DeviceEventEmitter.emit(Events.CRT_TOGGLED, serverUrl === currentServerUrl); -} +import EphemeralStore from '@store/ephemeral_store'; export async function handlePreferenceChangedEvent(serverUrl: string, msg: WebSocketMessage): Promise { - let database; - let operator; - try { - const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - database = result.database; - operator = result.operator; - } catch (e) { + if (EphemeralStore.isEnablingCRT()) { return; } try { + const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const preference: PreferenceType = JSON.parse(msg.data.preference); handleSavePostAdded(serverUrl, [preference]); const hasDiffNameFormatPref = await differsFromLocalNameFormat(database, [preference]); const crtToggled = await getHasCRTChanged(database, [preference]); - if (operator) { - await operator.handlePreferences({ - prepareRecordsOnly: false, - preferences: [preference], - }); - } + await operator.handlePreferences({ + prepareRecordsOnly: false, + preferences: [preference], + }); if (hasDiffNameFormatPref) { updateDmGmDisplayName(serverUrl); @@ -57,22 +41,22 @@ export async function handlePreferenceChangedEvent(serverUrl: string, msg: WebSo } export async function handlePreferencesChangedEvent(serverUrl: string, msg: WebSocketMessage): Promise { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { + if (EphemeralStore.isEnablingCRT()) { return; } + try { + const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const preferences: PreferenceType[] = JSON.parse(msg.data.preferences); handleSavePostAdded(serverUrl, preferences); - const hasDiffNameFormatPref = await differsFromLocalNameFormat(operator.database, preferences); - const crtToggled = await getHasCRTChanged(operator.database, preferences); - if (operator) { - await operator.handlePreferences({ - prepareRecordsOnly: false, - preferences, - }); - } + const hasDiffNameFormatPref = await differsFromLocalNameFormat(database, preferences); + const crtToggled = await getHasCRTChanged(database, preferences); + + await operator.handlePreferences({ + prepareRecordsOnly: false, + preferences, + }); if (hasDiffNameFormatPref) { updateDmGmDisplayName(serverUrl); @@ -87,14 +71,10 @@ export async function handlePreferencesChangedEvent(serverUrl: string, msg: WebS } export async function handlePreferencesDeletedEvent(serverUrl: string, msg: WebSocketMessage): Promise { - const database = DatabaseManager.serverDatabases[serverUrl]; - if (!database) { - return; - } - try { + const databaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const preferences: PreferenceType[] = JSON.parse(msg.data.preferences); - deletePreferences(database, preferences); + deletePreferences(databaseAndOperator, preferences); } catch { // Do nothing } @@ -102,16 +82,17 @@ export async function handlePreferencesDeletedEvent(serverUrl: string, msg: WebS // If preferences include new save posts we fetch them async function handleSavePostAdded(serverUrl: string, preferences: PreferenceType[]) { - const database = DatabaseManager.serverDatabases[serverUrl]?.database; - if (!database) { - return; - } + try { + const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORIES.SAVED_POST); - const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORIES.SAVED_POST); - for await (const saved of savedPosts) { - const post = await getPostById(database, saved.name); - if (!post) { - await fetchPostById(serverUrl, saved.name, false); + for await (const saved of savedPosts) { + const post = await getPostById(database, saved.name); + if (!post) { + await fetchPostById(serverUrl, saved.name, false); + } } + } catch { + // Do nothing } } diff --git a/app/components/post_draft/post_input/post_input.tsx b/app/components/post_draft/post_input/post_input.tsx index fd09fe6ff..21fd757a0 100644 --- a/app/components/post_draft/post_input/post_input.tsx +++ b/app/components/post_draft/post_input/post_input.tsx @@ -281,7 +281,10 @@ export default function PostInput({ inputRef.current?.focus(); } }); - return () => listener.remove(); + return () => { + listener.remove(); + updateDraftMessage(serverUrl, channelId, rootId, value); // safe draft on unmount + }; }, [updateValue, value, channelId, rootId]); useEffect(() => { diff --git a/app/queries/servers/entry.ts b/app/queries/servers/entry.ts index bafed8942..23231d519 100644 --- a/app/queries/servers/entry.ts +++ b/app/queries/servers/entry.ts @@ -39,6 +39,7 @@ const { THREAD, THREADS_IN_TEAM, THREAD_PARTICIPANT, + TEAM_THREADS_SYNC, MY_CHANNEL, } = MM_TABLES.SERVER; @@ -99,6 +100,7 @@ export async function truncateCrtRelatedTables(serverUrl: string): Promise<{erro [`DELETE FROM ${THREAD}`, []], [`DELETE FROM ${THREADS_IN_TEAM}`, []], [`DELETE FROM ${THREAD_PARTICIPANT}`, []], + [`DELETE FROM ${TEAM_THREADS_SYNC}`, []], [`DELETE FROM ${MY_CHANNEL}`, []], ], }); diff --git a/app/screens/emoji_picker/picker/header/skintone_selector/skintone_selector.tsx b/app/screens/emoji_picker/picker/header/skintone_selector/skintone_selector.tsx index 8e5e84797..e95098796 100644 --- a/app/screens/emoji_picker/picker/header/skintone_selector/skintone_selector.tsx +++ b/app/screens/emoji_picker/picker/header/skintone_selector/skintone_selector.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {Platform, StyleSheet} from 'react-native'; +import {InteractionManager, Platform, StyleSheet} from 'react-native'; import Animated, { EntryAnimationsValues, ExitAnimationsValues, FadeIn, FadeOut, SharedValue, useAnimatedStyle, withDelay, withTiming, @@ -125,13 +125,11 @@ const SkinToneSelector = ({skinTone = 'default', containerWidth, isSearching, tu }, []); useEffect(() => { - const t = setTimeout(() => { + InteractionManager.runAfterInteractions(() => { if (!tutorialWatched) { setTooltipVisible(true); } - }, 750); - - return () => clearTimeout(t); + }); }, []); return ( diff --git a/app/screens/settings/display_crt/display_crt.tsx b/app/screens/settings/display_crt/display_crt.tsx index 155d8591c..309d445e1 100644 --- a/app/screens/settings/display_crt/display_crt.tsx +++ b/app/screens/settings/display_crt/display_crt.tsx @@ -4,7 +4,7 @@ import React, {useCallback, useState} from 'react'; import {useIntl} from 'react-intl'; -import {savePreference} from '@actions/remote/preference'; +import {handleCRTToggled, savePreference} from '@actions/remote/preference'; import SettingBlock from '@components/settings/block'; import SettingContainer from '@components/settings/container'; import SettingOption from '@components/settings/option'; @@ -15,6 +15,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import useBackNavigation from '@hooks/navigate_back'; import {t} from '@i18n'; import {popTopScreen} from '@screens/navigation'; +import EphemeralStore from '@store/ephemeral_store'; import type {AvailableScreens} from '@typings/screens/navigation'; @@ -36,7 +37,8 @@ const DisplayCRT = ({componentId, currentUserId, isCRTEnabled}: Props) => { const close = () => popTopScreen(componentId); - const saveCRTPreference = useCallback(() => { + const saveCRTPreference = useCallback(async () => { + close(); if (isCRTEnabled !== isEnabled) { const crtPreference: PreferenceType = { category: Preferences.CATEGORIES.DISPLAY_SETTINGS, @@ -44,9 +46,13 @@ const DisplayCRT = ({componentId, currentUserId, isCRTEnabled}: Props) => { user_id: currentUserId, value: isEnabled ? Preferences.COLLAPSED_REPLY_THREADS_ON : Preferences.COLLAPSED_REPLY_THREADS_OFF, }; - savePreference(serverUrl, [crtPreference]); + + EphemeralStore.setEnablingCRT(true); + const {error} = await savePreference(serverUrl, [crtPreference]); + if (!error) { + handleCRTToggled(serverUrl); + } } - close(); }, [isEnabled, isCRTEnabled, serverUrl]); useBackNavigation(saveCRTPreference); diff --git a/app/store/ephemeral_store.ts b/app/store/ephemeral_store.ts index 4431d958e..4a1f7857f 100644 --- a/app/store/ephemeral_store.ts +++ b/app/store/ephemeral_store.ts @@ -23,6 +23,7 @@ class EphemeralStore { private switchingToChannel = new Set(); private currentThreadId = ''; private notificationTapped = false; + private enablingCRT = false; // Ephemeral control when (un)archiving a channel locally addArchivingChannel = (channelId: string) => { @@ -120,6 +121,14 @@ class EphemeralStore { this.switchingToChannel.delete(channelId); }; + setEnablingCRT = (value: boolean) => { + this.enablingCRT = value; + }; + + isEnablingCRT = () => { + return this.enablingCRT; + }; + private getCanJoinOtherTeamsSubject = (serverUrl: string) => { if (!this.canJoinOtherTeams[serverUrl]) { this.canJoinOtherTeams[serverUrl] = new BehaviorSubject(false); diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8205a0c7c..3633b3e7b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -190,14 +190,16 @@ end desc 'Upload file to s3' lane :upload_file_to_s3 do |options| os_type = options[:os_type].downcase - extension = os_type == "android" ? "*.apk" : "*.ipa" + extensions = os_type == "android" ? ["*.apk", "*.aab"] : ["*.ipa"] build_folder_path = Dir[File.expand_path('..')].first files = [] unless options[:file].nil? || options[:file].empty? files.push("#{build_folder_path}/#{options[:file]}") else - files = Dir.glob("#{build_folder_path}/#{extension}").select { |f| File.file? f} + extensions.each do |extension| + files.push(*Dir.glob("#{build_folder_path}/#{extension}").select { |f| File.file? f}) + end end unless ENV['AWS_BUCKET_NAME'].nil? || ENV['AWS_BUCKET_NAME'].empty? || ENV['AWS_REGION'].nil? || ENV['AWS_REGION'].empty? || files.length == 0 @@ -636,7 +638,7 @@ platform :android do replace_assets link_sentry_android build_android - move_apk_to_root + move_android_to_root upload_file_to_s3({:os_type => "android"}) end @@ -750,10 +752,10 @@ platform :android do end lane :deploy do |options| - apk_path = options[:file] + file_path = options[:file] - unless apk_path.nil? - submit_to_google_play(apk_path) + unless file_path.nil? + submit_to_google_play(file_path) end end @@ -779,49 +781,73 @@ platform :android do def build_android config_mode = ENV['BUILD_FOR_RELEASE'] == 'true' ? 'Release' : 'Debug' + build_task = ENV['ANDROID_BUILD_TASK'].nil? || ENV['ANDROID_BUILD_TASK'].empty? ? 'assemble' : ENV['ANDROID_BUILD_TASK'] + build_task = build_task.split(",") - gradle( - task: 'app:assemble', - build_type: config_mode, - project_dir: 'android/', - properties: { - 'separateApk' => ENV["SEPARATE_APKS"] || false, - 'universalApk' => ENV["SEPARATE_APKS"] || false, - } - ) + if build_task.include?("assemble") + gradle( + task: 'app:assemble', + build_type: config_mode, + project_dir: 'android/', + properties: { + 'separateApk' => ENV["SEPARATE_APKS"] || false, + 'universalApk' => ENV["SEPARATE_APKS"] || false, + } + ) + end + + if build_task.include?("bundle") + gradle( + task: 'app:bundle', + build_type: config_mode, + project_dir: 'android/', + ) + end end - def move_apk_to_root + def move_android_to_root app_name = ENV['APP_NAME'] || 'Mattermost Beta' app_name_sub = app_name.gsub(" ", "_") - apks = lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS] - json_path = lane_context[SharedValues::GRADLE_OUTPUT_JSON_OUTPUT_PATH] + build_task = ENV['ANDROID_BUILD_TASK'].nil? || ENV['ANDROID_BUILD_TASK'].empty? ? 'assemble' : ENV['ANDROID_BUILD_TASK'] + build_task = build_task.split(",") - if json_path.nil? || json_path.empty? - dir_name = File.dirname(apks.first) - json_path = Dir.glob(File.join(dir_name, '*.json')).first - end - - UI.message("JSON PATH APK #{json_path}") - outputJson = load_config_json(json_path) - paths = [] - apks.each do |apk| - filename = File.basename(apk) - output = outputJson["elements"].find { |o| o["outputFile"] == filename } - unless output.nil? - filterName = output["filters"].empty? ? '' : output["filters"].first["value"] - name = "#{app_name_sub}" - unless filterName.nil? || filterName.empty? - name += "-#{filterName}" - end - new_apk_path = "#{Pathname.new(File.expand_path(File.dirname(__FILE__))).parent.to_s}/#{name}.apk" - paths.push(new_apk_path) - sh "mv #{apk} #{new_apk_path}" - UI.message("APK PATH \"#{new_apk_path}\", #{filterName}") + if build_task.include?("assemble") + apks = lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS] + json_path = lane_context[SharedValues::GRADLE_OUTPUT_JSON_OUTPUT_PATH] + + if json_path.nil? || json_path.empty? + dir_name = File.dirname(apks.first) + json_path = Dir.glob(File.join(dir_name, '*.json')).first end + + UI.message("JSON PATH APK #{json_path}") + outputJson = load_config_json(json_path) + paths = [] + apks.each do |apk| + filename = File.basename(apk) + output = outputJson["elements"].find { |o| o["outputFile"] == filename } + unless output.nil? + filterName = output["filters"].empty? ? '' : output["filters"].first["value"] + name = "#{app_name_sub}" + unless filterName.nil? || filterName.empty? + name += "-#{filterName}" + end + new_apk_path = "#{Pathname.new(File.expand_path(File.dirname(__FILE__))).parent.to_s}/#{name}.apk" + paths.push(new_apk_path) + sh "mv #{apk} #{new_apk_path}" + UI.message("APK PATH \"#{new_apk_path}\", #{filterName}") + end + end + + lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS] = paths end - lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS] = paths + if build_task.include?("bundle") + aab = lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH] + new_aab_path = "#{Pathname.new(File.expand_path(File.dirname(__FILE__))).parent.to_s}/#{app_name_sub}.aab" + sh "mv #{aab} #{new_aab_path}" + UI.message("AAB PATH \"#{new_aab_path}\"") + end end def link_sentry_android @@ -960,21 +986,36 @@ def submit_to_testflight(ipa_path) }) end -def submit_to_google_play(apk_path) - apks = Dir.glob(apk_path).select { |f| File.file? f} - filenames = apks.map {|f| File.basename(f)} - - if(apks.length > 0) +def submit_to_google_play(file_path) + filenames = [] + if(file_path.include?("aab")) + aab_filename = Dir.glob(file_path).select { |f| File.file? f}.first unless ENV['SUPPLY_JSON_KEY'].nil? || ENV['SUPPLY_JSON_KEY'].empty? supply( package_name: ENV['MAIN_APP_IDENTIFIER'] || ENV['SUPPLY_PACKAGE_NAME'], track: ENV['SUPPLY_TRACK'] || 'alpha', - apk_paths: apks + aab: aab_filename, + skip_upload_apk: true ) end + filenames.push(aab_filename) else - UI.user_error! "There are no apks in #{apk_path}" - return + apks = Dir.glob(file_path).select { |f| File.file? f} + filenames = apks.map {|f| File.basename(f)} + + if(apks.length > 0) + unless ENV['SUPPLY_JSON_KEY'].nil? || ENV['SUPPLY_JSON_KEY'].empty? + supply( + package_name: ENV['MAIN_APP_IDENTIFIER'] || ENV['SUPPLY_PACKAGE_NAME'], + track: ENV['SUPPLY_TRACK'] || 'alpha', + apk_paths: apks, + skip_upload_aab: true + ) + end + else + UI.user_error! "There are no apks in #{apk_path}" + return + end end version_number = android_get_version_name(gradle_file: './android/app/build.gradle') diff --git a/ios/Gekidou/Sources/Gekidou/Cache/FileCache.swift b/ios/Gekidou/Sources/Gekidou/Cache/FileCache.swift deleted file mode 100644 index 118441fcc..000000000 --- a/ios/Gekidou/Sources/Gekidou/Cache/FileCache.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// FileCache.swift -// Gekidou -// -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -// - -import Foundation -import UIKit - -public class FileCache: NSObject { - private var cacheURL: URL? - @objc public static let `default` = FileCache() - - override private init() { - super.init() - let filemgr = FileManager.default - let appGroupId = Bundle.main.infoDictionary!["AppGroupIdentifier"] as! String - let containerUrl = filemgr.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) - if let url = containerUrl, - let cacheURL = url.appendingPathComponent("Library", isDirectory: true) as URL? { - self.cacheURL = cacheURL.appendingPathComponent("Caches", isDirectory: true) - self.createDirectoryIfNeeded(directory: self.cacheURL) - } - } - - private func createDirectoryIfNeeded(directory: URL?) { - var isDirectory = ObjCBool(false) - - if let cachePath = directory?.path { - let exists = FileManager.default.fileExists(atPath: cachePath, isDirectory: &isDirectory) - - if !exists && !isDirectory.boolValue { - try? FileManager.default.createDirectory(atPath: cachePath, withIntermediateDirectories: true, attributes: nil) - } - } - } - - private func getUrlImageFor(serverUrl: String, userId: String) -> URL? { - guard let url = cacheURL else {return nil} - - let serverCacheURL = url.appendingPathComponent(serverUrl.toUrlSafeBase64Encode(), isDirectory: true) - createDirectoryIfNeeded(directory: serverCacheURL) - return serverCacheURL.appendingPathComponent(userId + ".png") - } - - public func getProfileImage(serverUrl: String, userId: String) -> UIImage? { - guard let url = getUrlImageFor(serverUrl: serverUrl, userId: userId) else { return nil } - return UIImage(contentsOfFile: url.path) - } - - public func saveProfileImage(serverUrl: String, userId: String, imageData: Data?) { - do { - guard let data = imageData, - let url = getUrlImageFor(serverUrl: serverUrl, userId: userId) - else { return } - try data.write(to: url) - } catch let error { - print("Erro saving image. \(error)") - } - } -} diff --git a/ios/Gekidou/Sources/Gekidou/Cache/ImageCache+Get.swift b/ios/Gekidou/Sources/Gekidou/Cache/ImageCache+Get.swift index cb7c5530e..bf950bc42 100644 --- a/ios/Gekidou/Sources/Gekidou/Cache/ImageCache+Get.swift +++ b/ios/Gekidou/Sources/Gekidou/Cache/ImageCache+Get.swift @@ -1,7 +1,7 @@ import Foundation extension ImageCache { - func image(for userId: String, updatedAt: Double, forServer serverUrl: String) -> Data? { + public func image(for userId: String, updatedAt: Double, forServer serverUrl: String) -> Data? { lock.lock(); defer { lock.unlock() } let key = "\(serverUrl)-\(userId)-\(updatedAt)" as NSString if let image = imageCache.object(forKey: key) as? Data { diff --git a/ios/Gekidou/Sources/Gekidou/Cache/ImageCache+InsertRemove.swift b/ios/Gekidou/Sources/Gekidou/Cache/ImageCache+InsertRemove.swift index c8e1b60af..ee06088a0 100644 --- a/ios/Gekidou/Sources/Gekidou/Cache/ImageCache+InsertRemove.swift +++ b/ios/Gekidou/Sources/Gekidou/Cache/ImageCache+InsertRemove.swift @@ -6,7 +6,7 @@ extension ImageCache { keysCache.removeAllObjects() } - func insertImage(_ data: Data?, for userId: String, updatedAt: Double, forServer serverUrl: String ) { + public func insertImage(_ data: Data?, for userId: String, updatedAt: Double, forServer serverUrl: String ) { guard let data = data else { return removeImage(for: userId, forServer: serverUrl) } @@ -18,7 +18,7 @@ extension ImageCache { keysCache.setObject(imageKey, forKey: cacheKey) } - func removeImage(for userId: String, forServer serverUrl: String) { + public func removeImage(for userId: String, forServer serverUrl: String) { lock.lock(); defer { lock.unlock() } let cacheKey = "\(serverUrl)-\(userId)" as NSString if let key = keysCache.object(forKey: cacheKey) { diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift index be9d1b3f7..a77282245 100644 --- a/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift +++ b/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift @@ -70,7 +70,7 @@ extension Network { } public func fetchUserProfilePicture(userId: String, lastUpdateAt: Double, forServerUrl serverUrl: String, completionHandler: @escaping ResponseHandler) { - let endpoint = "/users/\(userId)/image?lastPictureUpdate=\(lastUpdateAt)" + let endpoint = "/users/\(userId)/image?lastPictureUpdate=\(Int64(lastUpdateAt))" let url = buildApiUrl(serverUrl, endpoint) return request(url, usingMethod: "GET", forServerUrl: serverUrl, completionHandler: completionHandler) diff --git a/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved index 44828fa2c..edc1406c8 100644 --- a/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Mattermost.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,7 +15,7 @@ "repositoryURL": "https://github.com/getsentry/sentry-cocoa.git", "state": { "branch": "8.0.0", - "revision": "4f6838af2688f8f762afdaea5bef941121e7d473", + "revision": "1a18683901844a2970ccfb633e4ebae565361817", "version": null } }, diff --git a/ios/Mattermost/Modules/SplitViewModule.swift b/ios/Mattermost/Modules/SplitViewModule.swift index ebf5adb26..0a4eacda1 100644 --- a/ios/Mattermost/Modules/SplitViewModule.swift +++ b/ios/Mattermost/Modules/SplitViewModule.swift @@ -32,7 +32,10 @@ class SplitViewModule: RCTEventEmitter { @objc func isRunningInFullScreen() -> Bool { guard let w = UIApplication.shared.delegate?.window, let window = w else { return false } - return window.frame.equalTo(window.screen.bounds) + let screenSize = window.screen.bounds.size.width + let frameSize = window.frame.size.width + let shouldBeConsideredFullScreen = frameSize >= (screenSize * 0.6) + return shouldBeConsideredFullScreen } @objc func isSplitView() { diff --git a/ios/MattermostShare/Utils/LocalFileManager.swift b/ios/MattermostShare/Utils/LocalFileManager.swift index 81f52d2d0..b0129fb2c 100644 --- a/ios/MattermostShare/Utils/LocalFileManager.swift +++ b/ios/MattermostShare/Utils/LocalFileManager.swift @@ -96,26 +96,9 @@ class LocalFileManager { return nil } - func saveProfileImage(image: UIImage, userId: String) { - guard let data = image.pngData(), - let url = getURLForImage(imageName: userId) - else { return } - - do { - try data.write(to: url) - } catch let error { - print("Erro saving image. \(error)") - } - } - - func getProfileImage(userId: String) -> UIImage? { - guard let url = getURLForImage(imageName: userId) else { return nil } - return UIImage(contentsOfFile: url.path) - } - func getImagePixels(imageUrl: URL) -> Int64 { if let imageSourceRef = CGImageSourceCreateWithURL(imageUrl as CFURL, nil), - let props = CGImageSourceCopyPropertiesAtIndex(imageSourceRef, 0, nil) as? NSDictionary, + let props: NSDictionary = CGImageSourceCopyPropertiesAtIndex(imageSourceRef, 0, nil), let height = props["PixelHeight"] as? NSNumber, let width = props["PixelWidth"] as? NSNumber { return Int64(width.intValue * height.intValue) diff --git a/ios/MattermostShare/ViewModels/ShareViewModel.swift b/ios/MattermostShare/ViewModels/ShareViewModel.swift index fa5a0470c..564a5ed51 100644 --- a/ios/MattermostShare/ViewModels/ShareViewModel.swift +++ b/ios/MattermostShare/ViewModels/ShareViewModel.swift @@ -78,20 +78,25 @@ class ShareViewModel: ObservableObject { } func getProfileImage(serverUrl: String, userId: String, imageBinding: Binding) { - if let image = fileManager.getProfileImage(userId: userId) { - imageBinding.wrappedValue = image + var lastUpdateAt: Double = 0 + if let updateAt = Gekidou.Database.default.getUserLastPictureAt(for: userId, forServerUrl: serverUrl) { + lastUpdateAt = updateAt + } + + if let data = Gekidou.ImageCache.default.image(for: userId, updatedAt: lastUpdateAt, forServer: serverUrl) { + imageBinding.wrappedValue = UIImage(data: data) } else { - downloadProfileImage(serverUrl: serverUrl, userId: userId, imageBinding: imageBinding) + downloadProfileImage(serverUrl: serverUrl, userId: userId, lastUpdateAt: lastUpdateAt, imageBinding: imageBinding) } } - func downloadProfileImage(serverUrl: String, userId: String, imageBinding: Binding) { + func downloadProfileImage(serverUrl: String, userId: String, lastUpdateAt: Double, imageBinding: Binding) { guard let _ = URL(string: serverUrl) else { debugPrint("Missing or Malformed URL") return } - Gekidou.Network.default.fetchUserProfilePicture(userId: userId, lastUpdateAt: 0, forServerUrl: serverUrl, completionHandler: {data, response, error in + Gekidou.Network.default.fetchUserProfilePicture(userId: userId, lastUpdateAt: lastUpdateAt, forServerUrl: serverUrl, completionHandler: {data, response, error in guard (response as? HTTPURLResponse)?.statusCode == 200 else { debugPrint("Error while fetching image \(String(describing: (response as? HTTPURLResponse)?.statusCode))") return @@ -100,9 +105,7 @@ class ShareViewModel: ObservableObject { if let data = data { let image = UIImage(data: data) imageBinding.wrappedValue = image - if let img = image { - self.fileManager.saveProfileImage(image: img, userId: userId) - } + Gekidou.ImageCache.default.insertImage(data, for: userId, updatedAt: lastUpdateAt, forServer: serverUrl) } }) }