Merge branch 'stable'

This commit is contained in:
Evgeny Poberezkin 2025-01-13 17:42:14 +00:00
commit 49bf3cc673
No known key found for this signature in database
GPG key ID: 494BDDD9A28B577D
8 changed files with 99 additions and 27 deletions

View file

@ -987,6 +987,7 @@ final class ChatModel: ObservableObject {
if let i = getChatIndex(id) { if let i = getChatIndex(id) {
let removed = chats.remove(at: i) let removed = chats.remove(at: i)
ChatTagsModel.shared.removePresetChatTags(removed.chatInfo, removed.chatStats) ChatTagsModel.shared.removePresetChatTags(removed.chatInfo, removed.chatStats)
removeWallpaperFilesFromChat(removed)
} }
} }
} }
@ -1025,6 +1026,23 @@ final class ChatModel: ObservableObject {
_ = upsertGroupMember(groupInfo, updatedMember) _ = upsertGroupMember(groupInfo, updatedMember)
} }
} }
func removeWallpaperFilesFromChat(_ chat: Chat) {
if case let .direct(contact) = chat.chatInfo {
removeWallpaperFilesFromTheme(contact.uiThemes)
} else if case let .group(groupInfo) = chat.chatInfo {
removeWallpaperFilesFromTheme(groupInfo.uiThemes)
}
}
func removeWallpaperFilesFromAllChats(_ user: User) {
// Currently, only removing everything from currently active user is supported. Inactive users are TODO
if user.userId == currentUser?.userId {
chats.forEach {
removeWallpaperFilesFromChat($0)
}
}
}
} }
struct ShowingInvitation { struct ShowingInvitation {

View file

@ -298,6 +298,7 @@ struct UserProfilesView: View {
private func removeUser(_ user: User, _ delSMPQueues: Bool, viewPwd: String?) async { private func removeUser(_ user: User, _ delSMPQueues: Bool, viewPwd: String?) async {
do { do {
if user.activeUser { if user.activeUser {
ChatModel.shared.removeWallpaperFilesFromAllChats(user)
if let newActive = m.users.first(where: { u in !u.user.activeUser && !u.user.hidden }) { if let newActive = m.users.first(where: { u in !u.user.activeUser && !u.user.hidden }) {
try await changeActiveUserAsync_(newActive.user.userId, viewPwd: nil) try await changeActiveUserAsync_(newActive.user.userId, viewPwd: nil)
try await deleteUser() try await deleteUser()
@ -323,6 +324,7 @@ struct UserProfilesView: View {
func deleteUser() async throws { func deleteUser() async throws {
try await apiDeleteUser(user.userId, delSMPQueues, viewPwd: viewPwd) try await apiDeleteUser(user.userId, delSMPQueues, viewPwd: viewPwd)
removeWallpaperFilesFromTheme(user.uiThemes)
await MainActor.run { withAnimation { m.removeUser(user) } } await MainActor.run { withAnimation { m.removeUser(user) } }
} }
} }

View file

@ -267,17 +267,26 @@ public func saveWallpaperFile(image: UIImage) -> String? {
public func removeWallpaperFile(fileName: String? = nil) { public func removeWallpaperFile(fileName: String? = nil) {
do { do {
try FileManager.default.contentsOfDirectory(atPath: getWallpaperDirectory().path).forEach { try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: getWallpaperDirectory().path), includingPropertiesForKeys: nil, options: []).forEach { url in
if URL(fileURLWithPath: $0).lastPathComponent == fileName { try FileManager.default.removeItem(atPath: $0) } if url.lastPathComponent == fileName {
try FileManager.default.removeItem(at: url)
}
} }
} catch { } catch {
logger.error("FileUtils.removeWallpaperFile error: \(error.localizedDescription)") logger.error("FileUtils.removeWallpaperFile error: \(error)")
} }
if let fileName { if let fileName {
WallpaperType.cachedImages.removeValue(forKey: fileName) WallpaperType.cachedImages.removeValue(forKey: fileName)
} }
} }
public func removeWallpaperFilesFromTheme(_ theme: ThemeModeOverrides?) {
if let theme {
removeWallpaperFile(fileName: theme.light?.wallpaper?.imageFile)
removeWallpaperFile(fileName: theme.dark?.wallpaper?.imageFile)
}
}
public func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String { public func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String {
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath) uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath)
} }

View file

@ -726,17 +726,11 @@ object ChatModel {
} }
fun removeChat(rhId: Long?, id: String) { fun removeChat(rhId: Long?, id: String) {
var removed: Chat? = null val i = getChatIndex(rhId, id)
chats.removeAll { if (i != -1) {
val found = it.id == id && it.remoteHostId == rhId val chat = chats.removeAt(i)
if (found) { removePresetChatTags(chat.chatInfo, chat.chatStats)
removed = it removeWallpaperFilesFromChat(chat)
}
found
}
removed?.let {
removePresetChatTags(it.chatInfo, it.chatStats)
} }
} }

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.ThemeModeOverrides
import chat.simplex.common.ui.theme.ThemeOverrides import chat.simplex.common.ui.theme.ThemeOverrides
import chat.simplex.common.views.chatlist.connectIfOpenedViaUri import chat.simplex.common.views.chatlist.connectIfOpenedViaUri
import chat.simplex.res.MR import chat.simplex.res.MR
@ -316,6 +317,30 @@ fun removeWallpaperFile(fileName: String? = null) {
WallpaperType.cachedImages.remove(fileName) WallpaperType.cachedImages.remove(fileName)
} }
fun removeWallpaperFilesFromTheme(theme: ThemeModeOverrides?) {
if (theme != null) {
removeWallpaperFile(theme.light?.wallpaper?.imageFile)
removeWallpaperFile(theme.dark?.wallpaper?.imageFile)
}
}
fun removeWallpaperFilesFromChat(chat: Chat) {
if (chat.chatInfo is ChatInfo.Direct) {
removeWallpaperFilesFromTheme(chat.chatInfo.contact.uiThemes)
} else if (chat.chatInfo is ChatInfo.Group) {
removeWallpaperFilesFromTheme(chat.chatInfo.groupInfo.uiThemes)
}
}
fun removeWallpaperFilesFromAllChats(user: User) {
// Currently, only removing everything from currently active user is supported. Inactive users are TODO
if (user.userId == chatModel.currentUser.value?.userId) {
chatModel.chats.value.forEach {
removeWallpaperFilesFromChat(it)
}
}
}
fun <T> createTmpFileAndDelete(dir: File = tmpDir, onCreated: (File) -> T): T { fun <T> createTmpFileAndDelete(dir: File = tmpDir, onCreated: (File) -> T): T {
val tmpFile = File(dir, UUID.randomUUID().toString()) val tmpFile = File(dir, UUID.randomUUID().toString())
tmpFile.parentFile.mkdirs() tmpFile.parentFile.mkdirs()

View file

@ -347,6 +347,7 @@ private suspend fun doRemoveUser(m: ChatModel, user: User, users: List<User>, de
try { try {
when { when {
user.activeUser -> { user.activeUser -> {
removeWallpaperFilesFromAllChats(user)
val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden } val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden }
if (newActive != null) { if (newActive != null) {
m.controller.changeActiveUser_(user.remoteHostId, newActive.userId, null) m.controller.changeActiveUser_(user.remoteHostId, newActive.userId, null)
@ -366,6 +367,7 @@ private suspend fun doRemoveUser(m: ChatModel, user: User, users: List<User>, de
m.controller.apiDeleteUser(user, delSMPQueues, viewPwd) m.controller.apiDeleteUser(user, delSMPQueues, viewPwd)
} }
} }
removeWallpaperFilesFromTheme(user.uiThemes)
m.removeUser(user) m.removeUser(user)
ntfManager.cancelNotificationsForUser(user.userId) ntfManager.cancelNotificationsForUser(user.userId)
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -106,7 +106,7 @@ import Simplex.Messaging.Version
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..)) import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..))
import Simplex.RemoteControl.Types (RCCtrlAddress (..)) import Simplex.RemoteControl.Types (RCCtrlAddress (..))
import System.Exit (ExitCode, exitSuccess) import System.Exit (ExitCode, exitSuccess)
import System.FilePath (takeFileName, (</>)) import System.FilePath (takeExtension, takeFileName, (</>))
import System.IO (Handle, IOMode (..)) import System.IO (Handle, IOMode (..))
import System.Random (randomRIO) import System.Random (randomRIO)
import UnliftIO.Async import UnliftIO.Async
@ -146,6 +146,15 @@ imageExtensions = [".jpg", ".jpeg", ".png", ".gif"]
fixedImagePreview :: ImageData fixedImagePreview :: ImageData
fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg==" fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="
imageFilePrefix :: String
imageFilePrefix = "IMG_"
voiceFilePrefix :: String
voiceFilePrefix = "voice_"
videoFilePrefix :: String
videoFilePrefix = "video_"
-- enableSndFiles has no effect when mainApp is True -- enableSndFiles has no effect when mainApp is True
startChatController :: Bool -> Bool -> CM' (Async ()) startChatController :: Bool -> Bool -> CM' (Async ())
startChatController mainApp enableSndFiles = do startChatController mainApp enableSndFiles = do
@ -897,7 +906,8 @@ processChatCommand' vr = \case
ifM ifM
(doesFileExist fsFromPath) (doesFileExist fsFromPath)
( do ( do
fsNewPath <- liftIO $ filesFolder `uniqueCombine` fileName newFileName <- liftIO $ maybe (pure fileName) (generateNewFileName fileName) $ mediaFilePrefix mc
fsNewPath <- liftIO $ filesFolder `uniqueCombine` newFileName
liftIO $ B.writeFile fsNewPath "" -- create empty file liftIO $ B.writeFile fsNewPath "" -- create empty file
encrypt <- chatReadVar encryptLocalFiles encrypt <- chatReadVar encryptLocalFiles
cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
@ -934,6 +944,17 @@ processChatCommand' vr = \case
when (B.length ch /= chSize') $ throwError $ CF.FTCEFileIOError "encrypting file: unexpected EOF" when (B.length ch /= chSize') $ throwError $ CF.FTCEFileIOError "encrypting file: unexpected EOF"
liftIO . CF.hPut w $ LB.fromStrict ch liftIO . CF.hPut w $ LB.fromStrict ch
when (size' > 0) $ copyChunks r w size' when (size' > 0) $ copyChunks r w size'
mediaFilePrefix :: MsgContent -> Maybe FilePath
mediaFilePrefix = \case
MCImage {} -> Just imageFilePrefix
MCVoice {} -> Just voiceFilePrefix
MCVideo {} -> Just videoFilePrefix
_ -> Nothing
generateNewFileName fileName prefix = do
currentDate <- liftIO getCurrentTime
let formattedDate = formatTime defaultTimeLocale "%Y%m%d_%H%M%S" currentDate
let ext = takeExtension fileName
pure $ prefix <> formattedDate <> ext
APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user
UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId
APIChatRead chatRef@(ChatRef cType chatId) -> withUser $ \_ -> case cType of APIChatRead chatRef@(ChatRef cType chatId) -> withUser $ \_ -> case cType of

View file

@ -785,8 +785,9 @@ testMultiForwardFiles =
bob <## " message without file" bob <## " message without file"
bob <# "@cath <- @alice" bob <# "@cath <- @alice"
bob <## " test_1.jpg"
bob <# "/f @cath test_1.jpg" jpgFileName <- T.unpack . T.strip . T.pack <$> getTermLine bob
bob <# ("/f @cath " <> jpgFileName)
bob <## "use /fc 5 to cancel sending" bob <## "use /fc 5 to cancel sending"
bob <# "@cath <- @alice" bob <# "@cath <- @alice"
@ -808,8 +809,8 @@ testMultiForwardFiles =
cath <## " message without file" cath <## " message without file"
cath <# "bob> -> forwarded" cath <# "bob> -> forwarded"
cath <## " test_1.jpg" cath <## (" " <> jpgFileName)
cath <# "bob> sends file test_1.jpg (136.5 KiB / 139737 bytes)" cath <# ("bob> sends file " <> jpgFileName <> " (136.5 KiB / 139737 bytes)")
cath <## "use /fr 1 [<dir>/ | <path>] to receive it" cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
cath <# "bob> -> forwarded" cath <# "bob> -> forwarded"
@ -824,15 +825,15 @@ testMultiForwardFiles =
cath <## "" cath <## ""
-- file transfer -- file transfer
bob <## "completed uploading file 5 (test_1.jpg) for cath" bob <## ("completed uploading file 5 (" <> jpgFileName <> ") for cath")
bob <## "completed uploading file 6 (test_1.pdf) for cath" bob <## "completed uploading file 6 (test_1.pdf) for cath"
cath ##> "/fr 1" cath ##> "/fr 1"
cath cath
<### [ "saving file 1 from bob to test_1.jpg", <### [ ConsoleString $ "saving file 1 from bob to " <> jpgFileName,
"started receiving file 1 (test_1.jpg) from bob" ConsoleString $ "started receiving file 1 (" <> jpgFileName <> ") from bob"
] ]
cath <## "completed receiving file 1 (test_1.jpg) from bob" cath <## ("completed receiving file 1 (" <> jpgFileName <> ") from bob")
cath ##> "/fr 2" cath ##> "/fr 2"
cath cath
@ -841,9 +842,9 @@ testMultiForwardFiles =
] ]
cath <## "completed receiving file 2 (test_1.pdf) from bob" cath <## "completed receiving file 2 (test_1.pdf) from bob"
src1B <- B.readFile "./tests/tmp/bob_app_files/test_1.jpg" src1B <- B.readFile ("./tests/tmp/bob_app_files/" <> jpgFileName)
src1B `shouldBe` dest1 src1B `shouldBe` dest1
dest1C <- B.readFile "./tests/tmp/cath_app_files/test_1.jpg" dest1C <- B.readFile ("./tests/tmp/cath_app_files/" <> jpgFileName)
dest1C `shouldBe` src1B dest1C `shouldBe` src1B
src2B <- B.readFile "./tests/tmp/bob_app_files/test_1.pdf" src2B <- B.readFile "./tests/tmp/bob_app_files/test_1.pdf"
@ -886,5 +887,5 @@ testMultiForwardFiles =
checkActionDeletesFile "./tests/tmp/bob_app_files/test.jpg" $ do checkActionDeletesFile "./tests/tmp/bob_app_files/test.jpg" $ do
bob ##> "/clear alice" bob ##> "/clear alice"
bob <## "alice: all messages are removed locally ONLY" bob <## "alice: all messages are removed locally ONLY"
fwdFileExists <- doesFileExist "./tests/tmp/bob_app_files/test_1.jpg" fwdFileExists <- doesFileExist ("./tests/tmp/bob_app_files/" <> jpgFileName)
fwdFileExists `shouldBe` True fwdFileExists `shouldBe` True