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:
parent
2fc1386b78
commit
d61fbd3180
17 changed files with 186 additions and 206 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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}`, []],
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"repositoryURL": "https://github.com/getsentry/sentry-cocoa.git",
|
||||
"state": {
|
||||
"branch": "8.0.0",
|
||||
"revision": "4f6838af2688f8f762afdaea5bef941121e7d473",
|
||||
"revision": "1a18683901844a2970ccfb633e4ebae565361817",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue