Merge branch 'stable' into stable-android

This commit is contained in:
Evgeny Poberezkin 2025-02-14 23:39:38 +00:00
commit a90c5b9cc6
No known key found for this signature in database
GPG key ID: 494BDDD9A28B577D
27 changed files with 227 additions and 31 deletions

View file

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

View file

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

View 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
}
}

View file

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

View file

@ -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 */,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 145 KiB

5
media-logos/README.md Normal file
View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View 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

View file

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

View file

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

View file

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

View file

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

View file

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