Various fixes (#7161)

* Save message draft when post input is unmounted

* Fix switch CRT on/off

* Handle iPad on Stage Manager

* iOS Share Extension to use LRU cache instead of file cache

* Support building android as aab

* use handleReconnect instead of appEntry on handleCRTToggled

* show skin tone selector tutorial after running all interactions

* Update app/actions/remote/preference.ts

Co-authored-by: Daniel Espino García <larkox@gmail.com>

* fix lint

---------

Co-authored-by: Daniel Espino García <larkox@gmail.com>
This commit is contained in:
Elias Nahum 2023-02-24 13:02:05 +02:00 committed by GitHub
parent 2fc1386b78
commit d61fbd3180
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 186 additions and 206 deletions

5
.gitignore vendored
View file

@ -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

View file

@ -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);
};

View file

@ -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<void> {
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<void> {
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<void> {
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
}
}

View file

@ -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(() => {

View file

@ -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}`, []],
],
});

View file

@ -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 (

View file

@ -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);

View file

@ -23,6 +23,7 @@ class EphemeralStore {
private switchingToChannel = new Set<string>();
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);

View file

@ -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')

View file

@ -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)")
}
}
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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)

View file

@ -15,7 +15,7 @@
"repositoryURL": "https://github.com/getsentry/sentry-cocoa.git",
"state": {
"branch": "8.0.0",
"revision": "4f6838af2688f8f762afdaea5bef941121e7d473",
"revision": "1a18683901844a2970ccfb633e4ebae565361817",
"version": null
}
},

View file

@ -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() {

View file

@ -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)

View file

@ -78,20 +78,25 @@ class ShareViewModel: ObservableObject {
}
func getProfileImage(serverUrl: String, userId: String, imageBinding: Binding<UIImage?>) {
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<UIImage?>) {
func downloadProfileImage(serverUrl: String, userId: String, lastUpdateAt: Double, imageBinding: Binding<UIImage?>) {
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)
}
})
}