mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-16 17:39:08 +00:00
Merge branch 'stable'
This commit is contained in:
@@ -5143,6 +5143,10 @@ public enum SimplexLinkType: String, Decodable, Hashable {
|
||||
|
||||
public struct SimplexNameInfo: Decodable, Equatable, Hashable {
|
||||
public var nameType: SimplexNameType
|
||||
public var nameDomain: SimplexNameDomain
|
||||
}
|
||||
|
||||
public struct SimplexNameDomain: Decodable, Equatable, Hashable {
|
||||
public var nameTLD: SimplexTLD
|
||||
public var domain: String
|
||||
public var subDomain: [String]
|
||||
|
||||
@@ -4764,6 +4764,11 @@ enum class SimplexLinkType(val linkType: String) {
|
||||
@Serializable
|
||||
data class SimplexNameInfo(
|
||||
val nameType: SimplexNameType,
|
||||
val nameDomain: SimplexNameDomain
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SimplexNameDomain(
|
||||
val nameTLD: SimplexTLD,
|
||||
val domain: String,
|
||||
val subDomain: List<String>
|
||||
|
||||
+3
@@ -217,6 +217,9 @@ class SelectionManager {
|
||||
val hi = maxOf(r.startIndex, r.endIndex)
|
||||
return (lo..hi).mapNotNull { idx ->
|
||||
val ci = items.getOrNull(idx)?.newest()?.item ?: return@mapNotNull null
|
||||
// Only real messages are copyable. Event/info items (e.g. "connected", calls, e2ee info)
|
||||
// have no msgContent and are never highlighted as selected, so they must never be copied.
|
||||
if (ci.content.msgContent == null) return@mapNotNull null
|
||||
if (ci.meta.itemDeleted != null && (!revealedItems.contains(ci.id) || ci.isDeletedContent)) return@mapNotNull null
|
||||
val sel = selectedRange(range, idx) ?: return@mapNotNull null
|
||||
selectedItemCopiedText(ci, sel, linkMode)
|
||||
|
||||
+12
-3
@@ -165,6 +165,7 @@ This file is generated automatically.
|
||||
- [SecurityCode](#securitycode)
|
||||
- [SimplePreference](#simplepreference)
|
||||
- [SimplexLinkType](#simplexlinktype)
|
||||
- [SimplexNameDomain](#simplexnamedomain)
|
||||
- [SimplexNameInfo](#simplexnameinfo)
|
||||
- [SimplexNameType](#simplexnametype)
|
||||
- [SimplexTLD](#simplextld)
|
||||
@@ -3445,15 +3446,23 @@ A_QUEUE:
|
||||
- "relay"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SimplexNameDomain
|
||||
|
||||
**Record type**:
|
||||
- nameTLD: [SimplexTLD](#simplextld)
|
||||
- domain: string
|
||||
- subDomain: [string]
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SimplexNameInfo
|
||||
|
||||
**Record type**:
|
||||
- nameType: [SimplexNameType](#simplexnametype)
|
||||
- nameTLD: [SimplexTLD](#simplextld)
|
||||
- domain: string
|
||||
- subDomain: [string]
|
||||
- nameDomain: [SimplexNameDomain](#simplexnamedomain)
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -345,6 +345,7 @@ chatTypesDocsData =
|
||||
(sti @SecurityCode, STRecord, "", [], "", ""),
|
||||
(sti @SimplePreference, STRecord, "", [], "", ""),
|
||||
(sti @SimplexLinkType, STEnum, "XL", [], "", ""),
|
||||
(sti @SimplexNameDomain, STRecord, "", [], "", ""),
|
||||
(sti @SimplexNameInfo, STRecord, "", [], "", ""),
|
||||
(sti @SimplexNameType, STEnum, "NT", [], "", ""),
|
||||
(sti @SimplexTLD, STEnum, "TLD", [], "", ""),
|
||||
@@ -561,6 +562,7 @@ deriving instance Generic RelayStatus
|
||||
deriving instance Generic ReportReason
|
||||
deriving instance Generic SecurityCode
|
||||
deriving instance Generic SimplexLinkType
|
||||
deriving instance Generic SimplexNameDomain
|
||||
deriving instance Generic SimplexNameInfo
|
||||
deriving instance Generic SimplexNameType
|
||||
deriving instance Generic SimplexTLD
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 04960864c4ec958c21c20e2a7d618524138b2e72
|
||||
tag: 61ee188ee0839c34de16bc17934f04ebc7fd4873
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -3850,13 +3850,17 @@ export enum SimplexLinkType {
|
||||
Relay = "relay",
|
||||
}
|
||||
|
||||
export interface SimplexNameInfo {
|
||||
nameType: SimplexNameType
|
||||
export interface SimplexNameDomain {
|
||||
nameTLD: SimplexTLD
|
||||
domain: string
|
||||
subDomain: string[]
|
||||
}
|
||||
|
||||
export interface SimplexNameInfo {
|
||||
nameType: SimplexNameType
|
||||
nameDomain: SimplexNameDomain
|
||||
}
|
||||
|
||||
export enum SimplexNameType {
|
||||
PublicGroup = "publicGroup",
|
||||
Contact = "contact",
|
||||
|
||||
@@ -2689,12 +2689,15 @@ class SimplePreference(TypedDict):
|
||||
|
||||
SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"]
|
||||
|
||||
class SimplexNameInfo(TypedDict):
|
||||
nameType: "SimplexNameType"
|
||||
class SimplexNameDomain(TypedDict):
|
||||
nameTLD: "SimplexTLD"
|
||||
domain: str
|
||||
subDomain: list[str]
|
||||
|
||||
class SimplexNameInfo(TypedDict):
|
||||
nameType: "SimplexNameType"
|
||||
nameDomain: "SimplexNameDomain"
|
||||
|
||||
SimplexNameType = Literal["publicGroup", "contact"]
|
||||
|
||||
SimplexTLD = Literal["simplex", "testing", "web"]
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Desktop: text selection copies non-message event items
|
||||
|
||||
Branch: `nd/fix-copy-non-msg-items` · code commit `a536452ca` · PR [#6993](https://github.com/simplex-chat/simplex-chat/pull/6993).
|
||||
|
||||
## 1. Problem statement
|
||||
|
||||
The Desktop "select text in messages" feature (PR [#6725](https://github.com/simplex-chat/simplex-chat/pull/6725)) lets the user drag a selection across several message bubbles and copy it. When the selection spans a chat event/info item — a "connected" event, a member "joined"/"left" event, a call event, an e2ee-info line, a feature-change line — the copied text includes that item's text, even though the item is never shown highlighted as part of the selection.
|
||||
|
||||
Expected: only real message text is copied. Observed: event/info text such as "connected" is appended into the clipboard between the selected messages.
|
||||
|
||||
### Privacy note
|
||||
|
||||
Event/info item text is produced from localized string resources (`generalGetString`, `RcvConnEvent.text`, etc.) — it is rendered in the language the user has chosen for the app, whereas real message text is not. A user who selects and copies a long span of messages carelessly, then pastes it into another chat or app, can therefore leak their chosen interface language through the event lines mixed into the paste. For a privacy-focused messenger this is a metadata leak, not only a cosmetic bug.
|
||||
|
||||
## 2. Root cause
|
||||
|
||||
`SelectionManager.getSelectedCopiedText` (`TextSelection.kt`) builds the copied string by iterating every merged-item index between the selection bounds and emitting each item's text:
|
||||
|
||||
```kotlin
|
||||
return (lo..hi).mapNotNull { idx ->
|
||||
val ci = items.getOrNull(idx)?.newest()?.item ?: return@mapNotNull null
|
||||
if (ci.meta.itemDeleted != null && ...) return@mapNotNull null
|
||||
val sel = selectedRange(range, idx) ?: return@mapNotNull null
|
||||
selectedItemCopiedText(ci, sel, linkMode)
|
||||
}
|
||||
```
|
||||
|
||||
For an *interior* item in a multi-item selection, `selectedRange` returns `0 until Int.MAX_VALUE` — the whole item is treated as selected — so its text is emitted unconditionally. The only items previously skipped were deleted ones.
|
||||
|
||||
Anchor/focus character tracking (`setupItemSelection`) is wired up only for real message views (`FramedItemView`, `EmojiItemView`); event/info items never register offsets and never compute a highlight range. So an event item caught between two selected messages is invisible to the highlight but fully visible to `getSelectedCopiedText`. The copy logic and the on-screen selection disagreed.
|
||||
|
||||
The distinguishing property: a real message has `ci.content.msgContent != null`; every event/info `CIContent` variant returns `msgContent == null`.
|
||||
|
||||
## 3. Solution summary
|
||||
|
||||
`apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt` — one guard, +3 lines.
|
||||
|
||||
In `getSelectedCopiedText`, skip any item whose `content.msgContent` is null, alongside the existing deleted-item filter:
|
||||
|
||||
```kotlin
|
||||
if (ci.content.msgContent == null) return@mapNotNull null
|
||||
```
|
||||
|
||||
Only real messages now contribute copied text — which is exactly the set of items that are selectable and highlighted, so the clipboard matches the visible selection. `content.msgContent` is the existing model property used elsewhere to tell a real message apart from an event/info item.
|
||||
|
||||
## 4. Alternatives considered (and rejected)
|
||||
|
||||
- **Special-case only "connected" events.** Matches the literal report but leaves the identical bug for every other event/info item (joined/left, calls, e2ee info, feature changes) — same class, same language leak.
|
||||
- **Make event items non-selectable / consume the drag.** Larger change to the selection gesture; event items are already non-anchorable, and the bug is purely in the copy aggregation, not in the gesture.
|
||||
- **Filter at the call site (`onCopySelection`).** Duplicates the message/non-message distinction outside the one function that owns copied-text assembly; `getSelectedCopiedText` is the correct single source.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Fix `/start remote host` parser when iface name contains a space
|
||||
|
||||
## Problem
|
||||
|
||||
Picking a non-default interface (e.g. Windows `Ethernet 2`) on the "Link a mobile" screen and refreshing the QR code returns `error chat: error chat commandError Failed reading: empty`. The desktop UI sends `/start remote host new addr=… iface="Ethernet 2" port=…`; the chat backend rejects it as an unparseable command. Without a workaround the user can't pin a specific local interface for the mobile-link controller.
|
||||
|
||||
## Cause
|
||||
|
||||
`rcCtrlAddressP` parses the iface value with `jsonP <|> text1P` (`src/Simplex/Chat/Library/Commands.hs:5549`). `jsonP` calls `A.takeByteString`, consuming *all* remaining input, then runs `eitherDecodeStrict'`. When `port=…` follows `iface=…` the strict decode fails because the JSON value `"Ethernet 2"` has trailing junk after it, so attoparsec backtracks to `text1P` (`takeTill (== ' ')`). `text1P` stops at the first space — inside the JSON quotes — leaving `2" port=12345` which nothing downstream can consume, `A.endOfInput` fails, the whole `A.choice` exhausts and surfaces attoparsec's `empty` message. With an iface name that has no space (`"lo"`) the bug is invisible: text1P swallows the full quoted token and the rest parses, but the interface name is stored with literal quotes so the iface preference silently never matches a real adapter anyway.
|
||||
|
||||
## Fix
|
||||
|
||||
Replace `jsonP` with a bounded `quotedP` that consumes only the bytes between `"…"` and leaves trailing fields for the next parser. `text1P` is kept as the unquoted fallback. Two-line change in `Commands.hs` plus a pure regression test in `tests/RemoteTests.hs` that asserts `parseChatCommand` of `/start remote host new addr=192.168.1.5 iface="Ethernet 2" port=12345` produces `RCCtrlAddress _ "Ethernet 2"` with port `12345`.
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."04960864c4ec958c21c20e2a7d618524138b2e72" = "0bxirnczwxjdm8jdvqalf0jvllvpmvbxwvjcz6aq9z7k7rx8ijai";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."61ee188ee0839c34de16bc17934f04ebc7fd4873" = "0ap5khdfwzi9gzc96y916hngmbl3c4ivkbf33anmv2r8n15bkkp0";
|
||||
"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";
|
||||
|
||||
@@ -5550,7 +5550,8 @@ chatCommandP =
|
||||
addressAA = AddressSettings False <$> (Just . AutoAccept <$> (" incognito=" *> onOffP <|> pure False)) <*> autoReply
|
||||
businessAA = " business" *> (AddressSettings True (Just $ AutoAccept False) <$> autoReply)
|
||||
autoReply = optional (A.space *> msgContentP)
|
||||
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P))
|
||||
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (quotedP <|> text1P))
|
||||
quotedP = safeDecodeUtf8 <$> (A.char '"' *> A.takeTill (== '"') <* A.char '"')
|
||||
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
|
||||
char_ = optional . A.char
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Messaging.Agent.Protocol (SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
|
||||
import Simplex.Messaging.Agent.Protocol (SimplexNameDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Util ((<$$>))
|
||||
import System.Console.ANSI.Types
|
||||
@@ -381,7 +381,7 @@ command' :: Text -> Text -> FormattedText
|
||||
command' = FormattedText . Just . Command
|
||||
|
||||
sname :: SimplexNameType -> SimplexTLD -> Text -> [Text] -> Text -> Markdown
|
||||
sname nt ns dom sub txt = markdown (SimplexName $ SimplexNameInfo nt ns dom sub) (pfx <> txt)
|
||||
sname nt ns dom sub txt = markdown (SimplexName $ SimplexNameInfo nt (SimplexNameDomain ns dom sub)) (pfx <> txt)
|
||||
where
|
||||
pfx = case nt of NTPublicGroup -> "#"; NTContact -> "@"
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.List (find, isPrefixOf)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Simplex.Chat.Controller (ChatConfig (..), versionNumber)
|
||||
import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), versionNumber)
|
||||
import Simplex.Chat.Library.Commands (parseChatCommand)
|
||||
import qualified Simplex.Chat.Controller as Controller
|
||||
import Simplex.Chat.Mobile.File
|
||||
import Simplex.Chat.Remote (remoteFilesFolder)
|
||||
@@ -24,6 +25,7 @@ import Simplex.Chat.Remote.Types
|
||||
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
|
||||
import Simplex.Messaging.Encoding.String (strEncode)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec hiding (it)
|
||||
import UnliftIO
|
||||
@@ -32,6 +34,12 @@ import UnliftIO.Directory
|
||||
|
||||
remoteTests :: SpecWith TestParams
|
||||
remoteTests = describe "Remote" $ do
|
||||
describe "/start remote host parser" $ do
|
||||
it "parses iface name with a space followed by port=" $ \_ ->
|
||||
parseChatCommand "/start remote host new addr=192.168.1.5 iface=\"Ethernet 2\" port=12345"
|
||||
`shouldSatisfy` \case
|
||||
Right (StartRemoteHost Nothing (Just (RCCtrlAddress _ "Ethernet 2")) (Just 12345)) -> True
|
||||
_ -> False
|
||||
xdescribe "No compression" $ aroundWith (. ((False, False),)) runRemoteTests
|
||||
xdescribe "Mobile offers compression" $ aroundWith (. ((True, False),)) runRemoteTests
|
||||
xdescribe "Desktop offers compression" $ aroundWith (. ((False, True),)) runRemoteTests
|
||||
|
||||
Reference in New Issue
Block a user