Merge branch 'stable' into stable-android
|
@ -857,7 +857,10 @@ final class ChatModel: ObservableObject {
|
|||
|
||||
func removeChat(_ id: String) {
|
||||
withAnimation {
|
||||
chats.removeAll(where: { $0.id == id })
|
||||
if let i = getChatIndex(id) {
|
||||
let chat = chats.remove(at: i)
|
||||
removeWallpaperFilesFromChat(chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -895,6 +898,23 @@ final class ChatModel: ObservableObject {
|
|||
_ = 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 {
|
||||
|
|
|
@ -54,6 +54,13 @@ struct DeveloperView: View {
|
|||
settingsRow("internaldrive", color: theme.colors.secondary) {
|
||||
Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades)
|
||||
}
|
||||
NavigationLink {
|
||||
StorageView()
|
||||
.navigationTitle("Storage")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("internaldrive", color: theme.colors.secondary) { Text("Storage") }
|
||||
}
|
||||
} header: {
|
||||
Text("Developer options")
|
||||
}
|
||||
|
|
56
apps/ios/Shared/Views/UserSettings/StorageView.swift
Normal file
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// StorageView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Stanislav Dmitrenko on 13.01.2025.
|
||||
// Copyright © 2025 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct StorageView: View {
|
||||
@State var appGroupFiles: [String: Int64] = [:]
|
||||
@State var documentsFiles: [String: Int64] = [:]
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
directoryView("App group:", appGroupFiles)
|
||||
if !documentsFiles.isEmpty {
|
||||
directoryView("Documents:", documentsFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.onAppear {
|
||||
appGroupFiles = traverseFiles(in: getGroupContainerDirectory())
|
||||
documentsFiles = traverseFiles(in: getDocumentsDirectory())
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func directoryView(_ name: LocalizedStringKey, _ contents: [String: Int64]) -> some View {
|
||||
Text(name).font(.headline)
|
||||
ForEach(Array(contents), id: \.key) { (key, value) in
|
||||
Text(key).bold() + Text(" ") + Text("\(ByteCountFormatter.string(fromByteCount: value, countStyle: .binary))")
|
||||
}
|
||||
}
|
||||
|
||||
private func traverseFiles(in dir: URL) -> [String: Int64] {
|
||||
var res: [String: Int64] = [:]
|
||||
let fm = FileManager.default
|
||||
do {
|
||||
if let enumerator = fm.enumerator(at: dir, includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey, .fileAllocatedSizeKey]) {
|
||||
for case let url as URL in enumerator {
|
||||
let attrs = try url.resourceValues(forKeys: [/*.isDirectoryKey, .fileSizeKey,*/ .fileAllocatedSizeKey])
|
||||
let root = String(url.absoluteString.replacingOccurrences(of: dir.absoluteString, with: "").split(separator: "/")[0])
|
||||
res[root] = (res[root] ?? 0) + Int64(attrs.fileAllocatedSize ?? 0)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("Error traversing files: \(error)")
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
|
@ -298,6 +298,7 @@ struct UserProfilesView: View {
|
|||
private func removeUser(_ user: User, _ delSMPQueues: Bool, viewPwd: String?) async {
|
||||
do {
|
||||
if user.activeUser {
|
||||
ChatModel.shared.removeWallpaperFilesFromAllChats(user)
|
||||
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 deleteUser()
|
||||
|
@ -323,6 +324,7 @@ struct UserProfilesView: View {
|
|||
|
||||
func deleteUser() async throws {
|
||||
try await apiDeleteUser(user.userId, delSMPQueues, viewPwd: viewPwd)
|
||||
removeWallpaperFilesFromTheme(user.uiThemes)
|
||||
await MainActor.run { withAnimation { m.removeUser(user) } }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,6 +200,7 @@
|
|||
8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; };
|
||||
8CB3476C2CF5CFFA006787A5 /* Ink in Frameworks */ = {isa = PBXBuildFile; productRef = 8CB3476B2CF5CFFA006787A5 /* Ink */; };
|
||||
8CB3476E2CF5F58B006787A5 /* ConditionsWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */; };
|
||||
8CBC14862D357CDB00BBD901 /* StorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBC14852D357CDB00BBD901 /* StorageView.swift */; };
|
||||
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
|
||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
||||
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
|
||||
|
@ -548,6 +549,7 @@
|
|||
8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = "<group>"; };
|
||||
8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionsWebView.swift; sourceTree = "<group>"; };
|
||||
8CBC14852D357CDB00BBD901 /* StorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageView.swift; sourceTree = "<group>"; };
|
||||
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
|
||||
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
||||
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
|
||||
|
@ -943,6 +945,7 @@
|
|||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */,
|
||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */,
|
||||
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */,
|
||||
8CBC14852D357CDB00BBD901 /* StorageView.swift */,
|
||||
);
|
||||
path = UserSettings;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1453,6 +1456,7 @@
|
|||
8C7F8F0E2C19C0C100D16888 /* ViewModifiers.swift in Sources */,
|
||||
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */,
|
||||
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */,
|
||||
8CBC14862D357CDB00BBD901 /* StorageView.swift in Sources */,
|
||||
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */,
|
||||
5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */,
|
||||
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */,
|
||||
|
|
|
@ -3391,8 +3391,12 @@ extension MsgReaction: Decodable {
|
|||
let type = try container.decode(String.self, forKey: CodingKeys.type)
|
||||
switch type {
|
||||
case "emoji":
|
||||
let emoji = try container.decode(MREmojiChar.self, forKey: CodingKeys.emoji)
|
||||
self = .emoji(emoji: emoji)
|
||||
do {
|
||||
let emoji = try container.decode(MREmojiChar.self, forKey: CodingKeys.emoji)
|
||||
self = .emoji(emoji: emoji)
|
||||
} catch {
|
||||
self = .unknown(type: "emoji")
|
||||
}
|
||||
default:
|
||||
self = .unknown(type: type)
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ public func getDocumentsDirectory() -> URL {
|
|||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
}
|
||||
|
||||
func getGroupContainerDirectory() -> URL {
|
||||
public func getGroupContainerDirectory() -> URL {
|
||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
|
||||
}
|
||||
|
||||
|
|
|
@ -267,17 +267,26 @@ public func saveWallpaperFile(image: UIImage) -> String? {
|
|||
|
||||
public func removeWallpaperFile(fileName: String? = nil) {
|
||||
do {
|
||||
try FileManager.default.contentsOfDirectory(atPath: getWallpaperDirectory().path).forEach {
|
||||
if URL(fileURLWithPath: $0).lastPathComponent == fileName { try FileManager.default.removeItem(atPath: $0) }
|
||||
try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: getWallpaperDirectory().path), includingPropertiesForKeys: nil, options: []).forEach { url in
|
||||
if url.lastPathComponent == fileName {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("FileUtils.removeWallpaperFile error: \(error.localizedDescription)")
|
||||
logger.error("FileUtils.removeWallpaperFile error: \(error)")
|
||||
}
|
||||
if let 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 {
|
||||
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath)
|
||||
}
|
||||
|
|
|
@ -546,7 +546,11 @@ object ChatModel {
|
|||
}
|
||||
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
chats.removeAll { it.id == id && it.remoteHostId == rhId }
|
||||
val i = getChatIndex(rhId, id)
|
||||
if (i != -1) {
|
||||
val chat = chats.removeAt(i)
|
||||
removeWallpaperFilesFromChat(chat)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||
|
@ -2977,7 +2981,7 @@ sealed class MsgReaction {
|
|||
MREmojiChar.Heart -> "❤️"
|
||||
else -> emoji.value
|
||||
}
|
||||
is Unknown -> ""
|
||||
is Unknown -> "?"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -2999,8 +3003,13 @@ object MsgReactionSerializer : KSerializer<MsgReaction> {
|
|||
return if (json is JsonObject && "type" in json) {
|
||||
when(val t = json["type"]?.jsonPrimitive?.content ?: "") {
|
||||
"emoji" -> {
|
||||
val emoji = Json.decodeFromString<MREmojiChar>(json["emoji"].toString())
|
||||
if (emoji == null) MsgReaction.Unknown(t, json) else MsgReaction.Emoji(emoji)
|
||||
val msgReaction = try {
|
||||
val emoji = Json.decodeFromString<MREmojiChar>(json["emoji"].toString())
|
||||
MsgReaction.Emoji(emoji)
|
||||
} catch (e: Throwable) {
|
||||
MsgReaction.Unknown(t, json)
|
||||
}
|
||||
msgReaction
|
||||
}
|
||||
else -> MsgReaction.Unknown(t, json)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.compose.ui.unit.*
|
|||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.ThemeModeOverrides
|
||||
import chat.simplex.common.ui.theme.ThemeOverrides
|
||||
import chat.simplex.common.views.chatlist.connectIfOpenedViaUri
|
||||
import chat.simplex.res.MR
|
||||
|
@ -316,6 +317,30 @@ fun removeWallpaperFile(fileName: String? = null) {
|
|||
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(onCreated: (File) -> T): T {
|
||||
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
|
||||
tmpFile.deleteOnExit()
|
||||
|
|
|
@ -347,6 +347,7 @@ private suspend fun doRemoveUser(m: ChatModel, user: User, users: List<User>, de
|
|||
try {
|
||||
when {
|
||||
user.activeUser -> {
|
||||
removeWallpaperFilesFromAllChats(user)
|
||||
val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden }
|
||||
if (newActive != 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)
|
||||
}
|
||||
}
|
||||
removeWallpaperFilesFromTheme(user.uiThemes)
|
||||
m.removeUser(user)
|
||||
ntfManager.cancelNotificationsForUser(user.userId)
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -164,7 +164,7 @@ The main objective here is to establish the framework for comparing the security
|
|||
<sup>2</sup> Post-quantum cryptography is available in beta version, as opt-in only for direct conversations. See below how it will be rolled-out further.
|
||||
|
||||
Some columns are marked with a yellow checkmark:
|
||||
- when messages are padded, but not to a fixed size.
|
||||
- when messages are padded, but not to a fixed size (Briar pads messages to the size rounded up to 1024 bytes, Signal - to 160 bytes).
|
||||
- when repudiation does not include client-server connection. In case of Cwtch it appears that the presence of cryptographic signatures compromises repudiation (deniability), but it needs to be clarified.
|
||||
- when 2-factor key exchange is optional (via security code verification).
|
||||
- when post-quantum cryptography is only added to the initial key agreement and does not protect break-in recovery.
|
||||
|
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 145 KiB |
5
media-logos/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# SimpleX logos
|
||||
|
||||
Please use these logos in your publications about SimpleX Chat.
|
||||
|
||||
You can also use app screenshots and diagrams from our [blog posts](../blog/README.md) – please reference them.
|
BIN
media-logos/simplex-app-icon-dark.png
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
media-logos/simplex-app-icon-light.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
media-logos/simplex-logo-dark.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
media-logos/simplex-logo-light.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
media-logos/simplex-symbol-dark.png
Normal file
After Width: | Height: | Size: 18 KiB |
10
media-logos/simplex-symbol-dark.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="34" height="35" viewBox="0 0 34 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.02958 8.60922L8.622 14.2013L14.3705 8.45375L17.1669 11.2498L11.4183 16.9972L17.0114 22.5895L14.1373 25.4633L8.54422 19.871L2.79636 25.6187L0 22.8227L5.74794 17.075L0.155484 11.483L3.02958 8.60922Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0923 25.5156L16.944 22.6642L16.9429 22.6634L22.6467 16.9612L17.0513 11.3675L17.0523 11.367L14.2548 8.56979L8.65972 2.97535L11.5114 0.123963L17.1061 5.71849L22.8099 0.015625L25.6074 2.81285L19.9035 8.51562L25.4984 14.1099L31.2025 8.40729L34 11.2045L28.2958 16.907L33.8917 22.5017L31.0399 25.3531L25.4442 19.7584L19.7409 25.4611L25.3365 31.0559L22.4848 33.9073L16.8892 28.3124L11.1864 34.0156L8.38885 31.2184L14.0923 25.5156Z" fill="url(#paint0_linear_656_10815)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_656_10815" x1="12.8381" y1="-0.678252" x2="9.54355" y2="31.4493" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#01F1FF"/>
|
||||
<stop offset="1" stop-color="#0197FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
media-logos/simplex-symbol-light.png
Normal file
After Width: | Height: | Size: 19 KiB |
15
media-logos/simplex-symbol-light.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_14_10)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.02972 8.59396L8.62219 14.186L14.3703 8.43848L17.1668 11.2346L11.4182 16.982L17.0112 22.5742L14.1371 25.448L8.5441 19.8557L2.79651 25.6035L0 22.8074L5.74813 17.0597L0.155656 11.4678L3.02972 8.59396Z" fill="#023789"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0922 25.5L16.9434 22.6486L16.9423 22.6478L22.6464 16.9456L17.0512 11.3519L17.0518 11.3514L14.2542 8.55418L8.65961 2.95973L11.5114 0.108337L17.106 5.70288L22.8095 0L25.607 2.79722L19.903 8.5L25.4981 14.0943L31.2022 8.39169L33.9997 11.1889L28.2957 16.8914L33.8914 22.4861L31.0396 25.3375L25.4439 19.7428L19.7404 25.4454L25.3361 31.0403L22.4843 33.8917L16.8887 28.2968L11.1862 34L8.38867 31.2028L14.0922 25.5Z" fill="url(#paint0_linear_14_10)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_14_10" x1="12.8379" y1="-0.693875" x2="9.54344" y2="31.4337" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#01F1FF"/>
|
||||
<stop offset="1" stop-color="#0197FF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_14_10">
|
||||
<rect width="34" height="34" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -1,5 +1,5 @@
|
|||
name: simplex-chat
|
||||
version: 6.2.4.0
|
||||
version: 6.2.5.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
|
|
@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.2.4.0
|
||||
version: 6.2.5.0
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
|
|
@ -129,7 +129,7 @@ import Simplex.Messaging.Version
|
|||
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..))
|
||||
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
|
||||
import System.Exit (ExitCode, exitSuccess)
|
||||
import System.FilePath (takeFileName, (</>))
|
||||
import System.FilePath (takeExtension, takeFileName, (</>))
|
||||
import qualified System.FilePath as FP
|
||||
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush)
|
||||
import System.Random (randomRIO)
|
||||
|
@ -292,6 +292,15 @@ fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEA
|
|||
smallGroupsRcptsMemLimit :: Int
|
||||
smallGroupsRcptsMemLimit = 20
|
||||
|
||||
imageFilePrefix :: String
|
||||
imageFilePrefix = "IMG_"
|
||||
|
||||
voiceFilePrefix :: String
|
||||
voiceFilePrefix = "voice_"
|
||||
|
||||
videoFilePrefix :: String
|
||||
videoFilePrefix = "video_"
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
|
@ -1237,7 +1246,12 @@ processChatCommand' vr = \case
|
|||
ifM
|
||||
(doesFileExist fsFromPath)
|
||||
( do
|
||||
fsNewPath <- liftIO $ filesFolder `uniqueCombine` fileName
|
||||
newFileName <- case mc of
|
||||
MCImage {} -> liftIO $ generateNewFileName fileName imageFilePrefix
|
||||
MCVoice {} -> liftIO $ generateNewFileName fileName voiceFilePrefix
|
||||
MCVideo {} -> liftIO $ generateNewFileName fileName videoFilePrefix
|
||||
_ -> pure fileName
|
||||
fsNewPath <- liftIO $ filesFolder `uniqueCombine` newFileName
|
||||
liftIO $ B.writeFile fsNewPath "" -- create empty file
|
||||
encrypt <- chatReadVar encryptLocalFiles
|
||||
cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
|
||||
|
@ -1274,6 +1288,12 @@ processChatCommand' vr = \case
|
|||
when (B.length ch /= chSize') $ throwError $ CF.FTCEFileIOError "encrypting file: unexpected EOF"
|
||||
liftIO . CF.hPut w $ LB.fromStrict ch
|
||||
when (size' > 0) $ copyChunks r w size'
|
||||
generateNewFileName :: String -> String -> IO String
|
||||
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
|
||||
UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId
|
||||
APIChatRead chatRef@(ChatRef cType chatId) -> withUser $ \_ -> case cType of
|
||||
|
@ -8443,8 +8463,8 @@ chatCommandP =
|
|||
"/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <*> liveMessageP <* A.space <*> msgContentP),
|
||||
"/_delete item " *> (APIDeleteChatItem <$> chatRefP <*> _strP <*> _strP),
|
||||
"/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <*> _strP),
|
||||
"/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP),
|
||||
"/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP),
|
||||
"/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> (knownReaction <$?> jsonP)),
|
||||
"/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <* A.space <*> (knownReaction <$?> jsonP)),
|
||||
"/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP),
|
||||
"/_forward " *> (APIForwardChatItems <$> chatRefP <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP),
|
||||
"/_read user " *> (APIUserRead <$> A.decimal),
|
||||
|
|
|
@ -407,6 +407,13 @@ data MsgReaction = MREmoji {emoji :: MREmojiChar} | MRUnknown {tag :: Text, json
|
|||
emojiTag :: IsString a => a
|
||||
emojiTag = "emoji"
|
||||
|
||||
knownReaction :: MsgReaction -> Either String MsgReaction
|
||||
knownReaction = \case
|
||||
r@MREmoji {} -> Right r
|
||||
MRUnknown {} -> Left "unknown MsgReaction"
|
||||
|
||||
-- parseJSON for MsgReaction parses unknown emoji reactions as MRUnknown with type "emoji",
|
||||
-- allowing to add new emojis in a backwards compatible way - UI shows them as ?
|
||||
instance FromJSON MsgReaction where
|
||||
parseJSON (J.Object v) = do
|
||||
tag <- v .: "type"
|
||||
|
|
|
@ -9,9 +9,9 @@ import Control.Concurrent (threadDelay)
|
|||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.List (intercalate)
|
||||
import qualified Data.Text as T
|
||||
import System.Directory (copyFile, doesFileExist, removeFile)
|
||||
import Simplex.Chat (fixedImagePreview)
|
||||
import Simplex.Chat.Types (ImageData (..))
|
||||
import System.Directory (copyFile, doesFileExist, removeFile)
|
||||
import Test.Hspec hiding (it)
|
||||
|
||||
chatForwardTests :: SpecWith FilePath
|
||||
|
@ -740,7 +740,7 @@ testMultiForwardFiles =
|
|||
|
||||
-- IDs to forward
|
||||
let msgId1 = (read msgIdZero :: Int) + 1
|
||||
msgIds = intercalate "," $ map (show . (msgId1 +)) [0..5]
|
||||
msgIds = intercalate "," $ map (show . (msgId1 +)) [0 .. 5]
|
||||
bob ##> ("/_forward plan @2 " <> msgIds)
|
||||
bob <## "Files can be received: 1, 2, 3, 4"
|
||||
bob <## "5 message(s) out of 6 can be forwarded"
|
||||
|
@ -785,8 +785,9 @@ testMultiForwardFiles =
|
|||
bob <## " message without file"
|
||||
|
||||
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 <# "@cath <- @alice"
|
||||
|
@ -808,8 +809,8 @@ testMultiForwardFiles =
|
|||
cath <## " message without file"
|
||||
|
||||
cath <# "bob> -> forwarded"
|
||||
cath <## " test_1.jpg"
|
||||
cath <# "bob> sends file test_1.jpg (136.5 KiB / 139737 bytes)"
|
||||
cath <## (" " <> jpgFileName)
|
||||
cath <# ("bob> sends file " <> jpgFileName <> " (136.5 KiB / 139737 bytes)")
|
||||
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
|
||||
cath <# "bob> -> forwarded"
|
||||
|
@ -824,15 +825,15 @@ testMultiForwardFiles =
|
|||
cath <## ""
|
||||
|
||||
-- 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"
|
||||
|
||||
cath ##> "/fr 1"
|
||||
cath
|
||||
<### [ "saving file 1 from bob to test_1.jpg",
|
||||
"started receiving file 1 (test_1.jpg) from bob"
|
||||
<### [ ConsoleString $ "saving file 1 from bob to " <> jpgFileName,
|
||||
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
|
||||
|
@ -841,9 +842,9 @@ testMultiForwardFiles =
|
|||
]
|
||||
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
|
||||
dest1C <- B.readFile "./tests/tmp/cath_app_files/test_1.jpg"
|
||||
dest1C <- B.readFile ("./tests/tmp/cath_app_files/" <> jpgFileName)
|
||||
dest1C `shouldBe` src1B
|
||||
|
||||
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
|
||||
bob ##> "/clear alice"
|
||||
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
|
||||
|
|