diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index 70d6fa4aa8..8d96102daa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -158,7 +158,10 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> val uriDecrypted = remember(media.uri.path) { mutableStateOf(if (media.fileSource?.cryptoArgs == null) media.uri else media.fileSource.decryptedGet()) } val decrypted = uriDecrypted.value if (decrypted != null) { - VideoView(modifier, decrypted, preview, index == settledCurrentPage, close) + // settledCurrentPage finishes **only** when fully swiped + // So we use pagerState.currentPage that changes right away as the screen is being dragged + val isCurrentPage = index == pagerState.currentPage && kotlin.math.abs(pagerState.currentPageOffsetFraction) < 0.3f + VideoView(modifier, decrypted, preview, isCurrentPage, close) DisposableEffect(Unit) { onDispose { playersToRelease.add(decrypted) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index d0ba082adf..d3b8cdcb58 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -27,7 +27,7 @@ actual fun base64ToBitmap(base64ImageString: String): ImageBitmap { .removePrefix("data:image/jpg;base64,") return try { ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap() - } catch (e: IOException) { + } catch (e: Throwable) { Log.e(TAG, "base64ToBitmap error: $e") errorBitmap() } diff --git a/cabal.project b/cabal.project index 390890d258..9b2c41a86f 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: ca26c69937083deee43b8b2200ec9ef4c004ceac + tag: d7b90b841504e4fd37497faa15e943c625a2bc82 source-repository-package type: git diff --git a/docs/contributing/CODE.md b/docs/contributing/CODE.md index 6cdd7972f5..7ae6d176ac 100644 --- a/docs/contributing/CODE.md +++ b/docs/contributing/CODE.md @@ -2,7 +2,7 @@ This file provides guidance on coding style and approaches and on building the code. -## Code Style and Formatting +## Code Style, Formatting and Approaches The project uses **fourmolu** for Haskell code formatting. Configuration is in `fourmolu.yaml`. @@ -41,6 +41,16 @@ Some files that use CPP language extension cannot be formatted as a whole, so in - Never do refactoring unless it substantially reduces cost of solving the current problem, including the cost of refactoring - Aim to minimize the code changes - do what is minimally required to solve users' problems +**Document and code structure:** +- **Never move existing code or sections around** - add new content at appropriate locations without reorganizing existing structure. +- When adding new sections to documents, continue the existing numbering scheme. +- Minimize diff size - prefer small, targeted changes over reorganization. + +**Code analysis and review:** +- Trace data flows end-to-end: from origin, through storage/parameters, to consumption. Flag values that are discarded and reconstructed from partial data (e.g. extracted from a URI missing original fields) — this is usually a bug. +- Read implementations of called functions, not just signatures — if duplication involves a called function, check whether decomposing it resolves the duplication. +- Do not save time on analysis. Read every function in the data flow even when the interface seems clear — wrong assumptions about internals are the main source of missed bugs. + ### Haskell Extensions - `StrictData` enabled by default - Use STM for safe concurrency diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 31166762bc..8e385481b8 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ca26c69937083deee43b8b2200ec9ef4c004ceac" = "1p7jhxcbn95kddfwa5rjpzfx78fzic03wmy9dmh1mj3j14vyfn02"; + "https://github.com/simplex-chat/simplexmq.git"."d7b90b841504e4fd37497faa15e943c625a2bc82" = "03s8wnkgbpgm5i8jhjsbr2ifyna9sf84bf5311ni9f2p4cf3md7b"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 597d704523..0cfcb9ab09 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -3720,7 +3720,7 @@ processChatCommand vr nm = \case getShortLinkConnReq :: User -> ConnShortLink m -> CM (ConnectionRequestUri m, ConnLinkData m) getShortLinkConnReq user l = do l' <- restoreShortLink' l - (cReq, cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) l' + (FixedLinkData {linkConnReq = cReq}, cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) l' case cData of ContactLinkData _ UserContactData {direct} | not direct -> throwChatError CEUnsupportedConnReq _ -> pure () @@ -4155,7 +4155,7 @@ agentSubscriber :: CM' () agentSubscriber = do q <- asks $ subQ . smpAgent forever (atomically (readTBQueue q) >>= process) - `E.catchAny` \e -> do + `catchOwn` \e -> do eToView' $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) (AgentConnId "") Nothing E.throwIO e where @@ -4166,7 +4166,7 @@ agentSubscriber = do SAERcvFile -> processAgentMsgRcvFile corrId entId msg SAESndFile -> processAgentMsgSndFile corrId entId msg where - run action = action `catchAllErrors'` (eToView') + run action = action `catchAllOwnErrors'` eToView' type AgentSubResult = Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 2225a472d0..607839ed36 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -2271,7 +2271,7 @@ createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do joinAgentConnectionAsync :: User -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM (CommandId, ConnId) joinAgentConnectionAsync user enableNtfs cReqUri cInfo subMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFJoinConn - connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo PQSupportOff subMode + connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) Nothing enableNtfs cReqUri cInfo PQSupportOff subMode pure (cmdId, connId) allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> CM () diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 40b667365e..3272bc0115 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -227,8 +227,7 @@ instance StrEncoding AppMessageBinary where let msgId = if B.null msgId' then Nothing else Just (SharedMsgId msgId') pure AppMessageBinary {tag, msgId, body} -data MsgScope - = MSMember {memberId :: MemberId} -- Admins can use any member id; members can use only their own id +data MsgScope = MSMember {memberId :: MemberId} -- Admins can use any member id; members can use only their own id deriving (Eq, Show) $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "MS") ''MsgScope) @@ -644,6 +643,9 @@ maxEncodedMsgLength = 15602 maxCompressedMsgLength :: Int maxCompressedMsgLength = 13380 +maxDecompressedMsgLength :: Int +maxDecompressedMsgLength = 65536 + -- maxEncodedMsgLength - delta between MSG and INFO + 100 (returned for forward overhead) -- delta between MSG and INFO = e2eEncUserMsgLength (no PQ) - e2eEncConnInfoLength (no PQ) = 1008 maxEncodedInfoLength :: Int @@ -666,20 +668,24 @@ encodeChatMessage maxSize msg = do parseChatMessages :: ByteString -> [Either String AChatMessage] parseChatMessages "" = [Left "empty string"] -parseChatMessages s = case B.head s of - '{' -> [ACMsg SJson <$> J.eitherDecodeStrict' s] - '[' -> case J.eitherDecodeStrict' s of - Right v -> map parseItem v - Left e -> [Left e] - 'X' -> decodeCompressed (B.drop 1 s) - _ -> [ACMsg SBinary <$> (appBinaryToCM =<< strDecode s)] +parseChatMessages msg = case B.head msg of + 'X' -> decodeCompressed (B.tail msg) + c -> parseUncompressed c msg where + parseUncompressed c s = case c of + '{' -> [ACMsg SJson <$> J.eitherDecodeStrict' s] + '[' -> case J.eitherDecodeStrict' s of + Right v -> map parseItem v + Left e -> [Left e] + _ -> [ACMsg SBinary <$> (appBinaryToCM =<< strDecode s)] parseItem :: J.Value -> Either String AChatMessage parseItem v = ACMsg SJson <$> JT.parseEither parseJSON v decodeCompressed :: ByteString -> [Either String AChatMessage] decodeCompressed s' = case smpDecode s' of Left e -> [Left e] - Right (compressed :: L.NonEmpty Compressed) -> concatMap (either (pure . Left) parseChatMessages . decompress1) compressed + Right (compressed :: L.NonEmpty Compressed) -> concatMap (either (pure . Left) parseUncompressed' . decompress1 maxDecompressedMsgLength) compressed + parseUncompressed' "" = [Left "empty string"] + parseUncompressed' s = parseUncompressed (B.head s) s compressedBatchMsgBody_ :: MsgBody -> ByteString compressedBatchMsgBody_ = markCompressedBatch . smpEncode . (L.:| []) . compress1