mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-02 20:01:53 +00:00
Merge branch 'stable' into nd/fix-subscription-status-indicator
This commit is contained in:
@@ -191,9 +191,14 @@ private func handleTextTaps(
|
||||
}
|
||||
}
|
||||
}
|
||||
if let index, let (uri, browser) = attributedStringLink(s, for: index) {
|
||||
if let index, let (uri, browser, simplex) = attributedStringLink(s, for: index) {
|
||||
if browser {
|
||||
openBrowserAlert(uri: uri)
|
||||
} else if simplex, let url = URL(string: uri) {
|
||||
// SimpleX links target this same app (simplex: scheme / simplex.chat universal link),
|
||||
// so UIApplication.shared.open is dropped by iOS while the app is in the foreground.
|
||||
// Route to the in-app connect flow instead (same sink onOpenURL feeds).
|
||||
ChatModel.shared.appOpenUrl = url
|
||||
} else if let url = URL(string: uri) {
|
||||
UIApplication.shared.open(url)
|
||||
} else {
|
||||
@@ -203,9 +208,10 @@ private func handleTextTaps(
|
||||
})
|
||||
}
|
||||
|
||||
func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (String, Bool)? {
|
||||
func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (String, Bool, Bool)? {
|
||||
var linkURL: String?
|
||||
var browser: Bool = false
|
||||
var simplex: Bool = false
|
||||
s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in
|
||||
if index >= range.location && index < range.location + range.length {
|
||||
if let nameInfo = attrs[nameAttrKey] as? SimplexNameInfo {
|
||||
@@ -213,6 +219,7 @@ private func handleTextTaps(
|
||||
} else if let url = attrs[linkAttrKey] as? String {
|
||||
linkURL = url
|
||||
browser = attrs[webLinkAttrKey] != nil
|
||||
simplex = attrs[simplexLinkAttrKey] != nil
|
||||
} else if let showSecrets, let i = attrs[secretAttrKey] as? Int {
|
||||
if showSecrets.wrappedValue.contains(i) {
|
||||
showSecrets.wrappedValue.remove(i)
|
||||
@@ -225,7 +232,7 @@ private func handleTextTaps(
|
||||
stop.pointee = true
|
||||
}
|
||||
}
|
||||
return if let linkURL { (linkURL, browser) } else { nil }
|
||||
return if let linkURL { (linkURL, browser, simplex) } else { nil }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +257,8 @@ private let linkAttrKey = NSAttributedString.Key("chat.simplex.app.link")
|
||||
|
||||
private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink")
|
||||
|
||||
private let simplexLinkAttrKey = NSAttributedString.Key("chat.simplex.app.simplexLink")
|
||||
|
||||
private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret")
|
||||
|
||||
private let commandAttrKey = NSAttributedString.Key("chat.simplex.app.command")
|
||||
@@ -392,6 +401,7 @@ func messageText(
|
||||
attrs = linkAttrs()
|
||||
if !preview {
|
||||
attrs[linkAttrKey] = simplexUri
|
||||
attrs[simplexLinkAttrKey] = true
|
||||
handleTaps = true
|
||||
}
|
||||
if let s = text ?? (privacySimplexLinkModeDefault.get() == .description ? linkType.description : nil) {
|
||||
|
||||
@@ -26,7 +26,9 @@ struct ChatHelp: View {
|
||||
Button("connect to SimpleX Chat developers.") {
|
||||
dismissSettingsSheet()
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(simplexTeamURL)
|
||||
// simplexTeamURL targets this same app; route to the in-app connect flow
|
||||
// (UIApplication.shared.open is dropped for self-owned URLs in the foreground)
|
||||
ChatModel.shared.appOpenUrl = simplexTeamURL
|
||||
}
|
||||
}
|
||||
.padding(.top, 2)
|
||||
|
||||
@@ -390,7 +390,9 @@ struct SettingsView: View {
|
||||
Button("Send questions and ideas") {
|
||||
dismiss()
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(simplexTeamURL)
|
||||
// simplexTeamURL targets this same app; route to the in-app connect flow
|
||||
// (UIApplication.shared.open is dropped for self-owned URLs in the foreground)
|
||||
ChatModel.shared.appOpenUrl = simplexTeamURL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# iOS: open SimpleX links in chat messages via in-app connect flow
|
||||
|
||||
## Problem
|
||||
|
||||
On iOS, tapping a **SimpleX connection/invitation link inside message text** does nothing — it never reaches the connection flow. Reproduced on iPhone 17 (v6.5.2 and v6.5.5). On the same screens, tapping a web link (opens browser), a `mailto:`/`tel:` link, and the connection-link **card** all work. Notably it was **device-specific**: dead on an iPhone 17 but working on an iPhone 12 running the **same iOS version**, with only **one** SimpleX app installed.
|
||||
|
||||
## Root cause
|
||||
|
||||
Inline links are dispatched in `MsgContentView.handleTextTaps` (`apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift`):
|
||||
|
||||
- web links (`webLinkAttrKey`) → `openBrowserAlert` → `UIApplication.shared.open` (Safari)
|
||||
- everything else → `UIApplication.shared.open(url)`
|
||||
|
||||
SimpleX links fell into the second branch. Two facts make this the bug:
|
||||
|
||||
1. **The URI is always the `simplex:` custom scheme.** The core markdown parser normalizes every connection link to the `simplex:` scheme via `simplexConnReqUri` / `simplexShortLink` (`src/Simplex/Chat/Markdown.hs:344,353`), regardless of whether the message contained `https://simplex.chat/…` or `simplex:/…` (see `tests/MarkdownTests.hs`). So the tap always calls `UIApplication.shared.open("simplex:/contact#…")`.
|
||||
|
||||
2. **`simplex:` is registered to this app, and the app is in the foreground.** `UIApplication.shared.open` is an OS app-launch API: it asks iOS (LaunchServices) to resolve the scheme to its registered app and activate it. Here the registered app is SimpleX itself, already foregrounded. **Re-entering the same foreground app through `open()` is not a supported operation** — `open()` exists to hand a URL to a *different* app or the system. When the resolved target is the calling foreground app, the outcome is undefined: on some devices iOS still delivers the URL to `onOpenURL`, on others it is a silent no-op (`open` returns `false`, no error, no UI).
|
||||
|
||||
That undefined outcome is decided by device-local OS state (scheme resolution / launch services), which is why identical code + identical OS + identical single app behaved differently on the iPhone 12 (delivered → connected) and the iPhone 17 (no-op → dead). It is **not** an OS-version rule and **not** a multiple-handler conflict — both were ruled out (same OS; single install).
|
||||
|
||||
This also explains the full symptom matrix — only the path that re-enters the same app via `open()` is affected:
|
||||
|
||||
| Tapped | Dispatch | Target | Result |
|
||||
|---|---|---|---|
|
||||
| Web link | `openBrowserAlert` → `open()` | Safari (other app) | works |
|
||||
| `mailto:` / `tel:` | `open()` | Mail / Phone (other apps) | works |
|
||||
| Invite card | `planAndConnect` in-process | this app, no `open()` | works |
|
||||
| Inline SimpleX link | `open("simplex:…")` | this app (self), foreground | undefined → dead |
|
||||
|
||||
The underlying cause is using the **wrong mechanism**: an OS hand-off API to perform an **in-app** action. Every other connect path handles the connection in-process and never leaves the app:
|
||||
|
||||
- the card: `planAndConnect` directly (`FramedItemView.swift`)
|
||||
- the share extension: `ShareSheet.openExternalLink` sets `ChatModel.appOpenUrl`
|
||||
- multiplatform: `openVerifiedSimplexUri` → `connectIfOpenedViaUri` → `planAndConnect`
|
||||
|
||||
Inline links were the lone exception delegating to the OS, making them hostage to undefined self-open behavior.
|
||||
|
||||
## Fix
|
||||
|
||||
Restore the three-way dispatch the multiplatform clients use (`WEB_URL` / `OTHER_URL` / `SIMPLEX_URL`):
|
||||
|
||||
- web → `openBrowserAlert` (unchanged)
|
||||
- `mailto:` / `tel:` → `UIApplication.shared.open` (unchanged — these target other apps)
|
||||
- **SimpleX → `ChatModel.appOpenUrl`** — the same sink `onOpenURL` feeds, leading to `connectViaUrl` → `planAndConnect`, entirely **in-process** with no OS round-trip
|
||||
|
||||
SimpleX links are identified by a dedicated attribute key (`simplexLinkAttrKey`) set on the `.simplexLink` format, mirroring the multiplatform `SIMPLEX_URL` annotation tag, rather than sniffing the URL string — so all link types (contact, invitation, group, channel, relay) are covered.
|
||||
|
||||
This is correct regardless of the exact device-local trigger, because it removes the dependency on iOS re-delivering a self-owned URL. The invite card already proves the in-process path works on the affected device.
|
||||
|
||||
Also fixes the same issue for the **"Send questions and ideas"** (Settings) and **"connect to SimpleX Chat developers"** (chat help) buttons, which opened `simplexTeamURL` (a `simplex:` link) the same broken way.
|
||||
|
||||
## Scope
|
||||
|
||||
- `apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift` — three-way tap dispatch + `simplexLinkAttrKey`
|
||||
- `apps/ios/Shared/Views/UserSettings/SettingsView.swift`, `apps/ios/Shared/Views/ChatList/ChatHelp.swift` — route `simplexTeamURL` in-process
|
||||
|
||||
No behavior change for web / `mailto:` / `tel:` links.
|
||||
|
||||
## Verification
|
||||
|
||||
- Tap an inline SimpleX invitation/contact link in a received message → the connection sheet opens (on iPhone 17, where it was previously dead).
|
||||
- The two developer-contact buttons open the connect flow.
|
||||
- Web links still open the browser; `mailto:`/`tel:` still open Mail/Phone.
|
||||
- Optional, to confirm the device-local nature: open a `simplex:/contact#…` link from another app (e.g. Notes) on the affected device — if that is also dead there but works on a second device, it confirms the difference is device-local scheme resolution rather than app code.
|
||||
@@ -38,8 +38,9 @@ scripts/desktop/prepare-openssl-windows.sh
|
||||
|
||||
openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-3.0.15 | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g')
|
||||
rm -rf $BUILD_DIR 2>/dev/null || true
|
||||
# Existence of this directory produces build error: cabal's bug
|
||||
rm -rf dist-newstyle/src/direct-sq* 2>/dev/null || true
|
||||
# Existence of these directories produces build error: cabal's bug
|
||||
# (simplexmq is removed because cabal cannot delete its read-only git submodule pack files - blst, libbbs - on Windows)
|
||||
rm -rf dist-newstyle/src/direct-sq* dist-newstyle/src/simplexmq* 2>/dev/null || true
|
||||
rm cabal.project.local 2>/dev/null || true
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
echo "package direct-sqlcipher" >> cabal.project.local
|
||||
|
||||
@@ -3675,7 +3675,7 @@ processChatCommand cxt nm = \case
|
||||
profileToSend <-
|
||||
presentUserBadge user incognitoProfile $ case gInfo_ of
|
||||
Just gInfo_' ->
|
||||
let allowSimplexLinks = maybe True (groupFeatureUserAllowed SGFSimplexLinks) gInfo_'
|
||||
let allowSimplexLinks = maybe True groupUserAllowSimplexLinks gInfo_'
|
||||
in userProfileInGroup' user allowSimplexLinks incognitoProfile
|
||||
Nothing -> userProfileDirect user incognitoProfile Nothing True
|
||||
chatEvent <- case gInfo_ of
|
||||
@@ -3988,7 +3988,7 @@ processChatCommand cxt nm = \case
|
||||
conn <- createRelayConnection db cxt user (groupMemberId' relayMember) connId ConnPrepared chatV subMode
|
||||
pure (relayMember, conn, groupRelay)
|
||||
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
||||
allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
|
||||
allowSimplexLinks = groupUserAllowSimplexLinks gInfo
|
||||
GroupMember {memberId = relayMemberId} = relayMember
|
||||
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
|
||||
let relayInv = GroupRelayInvitation {
|
||||
|
||||
@@ -367,7 +367,7 @@ prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole
|
||||
prohibitedSimplexLinks :: GroupInfo -> GroupMember -> MsgContent -> Maybe MarkdownList -> Bool
|
||||
prohibitedSimplexLinks gInfo m mc ft =
|
||||
not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo)
|
||||
&& (isChatLink mc || maybe False (any ftIsSimplexLink) ft)
|
||||
&& (isChatLink mc || maybe False (any ftIsSimplexLink) ft || hasObfuscatedSimplexLink (msgContentText mc))
|
||||
where
|
||||
isChatLink = \case
|
||||
MCChat {} -> True
|
||||
@@ -1177,7 +1177,7 @@ introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn
|
||||
sendGroupMemberMessages user gInfo conn introEvts'
|
||||
|
||||
userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile
|
||||
userProfileInGroup user = userProfileInGroup' user . groupFeatureUserAllowed SGFSimplexLinks
|
||||
userProfileInGroup user = userProfileInGroup' user . groupUserAllowSimplexLinks
|
||||
{-# INLINE userProfileInGroup #-}
|
||||
|
||||
userProfileInGroup' :: User -> Bool -> Maybe Profile -> Profile
|
||||
@@ -1195,7 +1195,7 @@ memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, a
|
||||
memberKey = MemberKey <$> memberPubKey
|
||||
}
|
||||
where
|
||||
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g
|
||||
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && groupFeatureMemberAllowed SGFDirectMessages m g
|
||||
|
||||
redactedMemberProfile :: Bool -> Profile -> Profile
|
||||
redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDescr, image, peerType, badge} =
|
||||
@@ -1203,6 +1203,7 @@ redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDes
|
||||
where
|
||||
removeSimplexLink s
|
||||
| allowSimplexLinks = Just s
|
||||
| hasObfuscatedSimplexLink s = Nothing
|
||||
| otherwise = maybe (Just s) (\fts -> if any ftIsSimplexLink fts then Nothing else Just s) $ parseMaybeMarkdownList s
|
||||
|
||||
sendHistory :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
@@ -2129,7 +2130,7 @@ sendGroupMessages user gInfo scope asGroup members events = do
|
||||
_ -> False
|
||||
sendProfileUpdate = do
|
||||
let members' = filter (`supportsVersion` memberProfileUpdateVersion) members
|
||||
allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
|
||||
allowSimplexLinks = groupUserAllowSimplexLinks gInfo
|
||||
-- shouldSendProfileUpdate excludes incognito membership, so the badge is presented
|
||||
profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile p
|
||||
void $ sendGroupMessage' user gInfo members' $ XInfo profileUpdate
|
||||
|
||||
@@ -813,7 +813,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
XGrpMemInfo memId _memProfile
|
||||
| sameMemberId memId m -> do
|
||||
let GroupMember {memberId = membershipMemId} = membership
|
||||
allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
|
||||
allowSimplexLinks = groupUserAllowSimplexLinks gInfo
|
||||
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
|
||||
-- TODO update member profile
|
||||
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
@@ -2701,7 +2701,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
pure m
|
||||
where
|
||||
contentChanged = not (sameProfileContent (redactedMemberProfile allowSimplexLinks (fromLocalProfile p)) (redactedMemberProfile allowSimplexLinks p'))
|
||||
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo
|
||||
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo && groupFeatureMemberAllowed SGFDirectMessages m gInfo
|
||||
updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of
|
||||
Just bc | isMainBusinessMember bc m -> do
|
||||
g' <- withStore $ \db -> updateGroupProfileFromMember db user g p'
|
||||
@@ -3090,7 +3090,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
pure toMember
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
-- [incognito] send membership incognito profile, create direct connection as incognito
|
||||
let allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
|
||||
let allowSimplexLinks = groupUserAllowSimplexLinks gInfo
|
||||
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
|
||||
dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile
|
||||
-- [async agent commands] no continuation needed, but commands should be asynchronous for stability
|
||||
|
||||
@@ -18,6 +18,7 @@ import Control.Monad
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as AB
|
||||
import Data.Attoparsec.Text (Parser)
|
||||
import qualified Data.Attoparsec.Text as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
@@ -191,6 +192,16 @@ isLink = \case
|
||||
hasLinks :: MarkdownList -> Bool
|
||||
hasLinks = any $ \(FormattedText f _) -> maybe False isLink f
|
||||
|
||||
hasObfuscatedSimplexLink :: Text -> Bool
|
||||
hasObfuscatedSimplexLink t =
|
||||
fromRight False $ AB.parseOnly findLinkP $ encodeUtf8 $ T.filter (not . isSpace) t
|
||||
where
|
||||
findLinkP = do
|
||||
AB.skipWhile (\c -> c /= 's' && c /= 'h') -- links start only with "simplex:" or "https://"
|
||||
(True <$ (strP :: AB.Parser AConnectionLink))
|
||||
<|> (AB.anyChar *> findLinkP)
|
||||
<|> pure False
|
||||
|
||||
markdownP :: Parser Markdown
|
||||
markdownP = mconcat <$> A.many' fragmentP
|
||||
where
|
||||
|
||||
@@ -638,6 +638,12 @@ groupFeatureUserAllowed :: GroupFeatureRoleI f => SGroupFeature f -> GroupInfo -
|
||||
groupFeatureUserAllowed feature GroupInfo {membership = GroupMember {memberRole}, fullGroupPreferences} =
|
||||
groupFeatureMemberAllowed' feature memberRole fullGroupPreferences
|
||||
|
||||
-- A connection link in a profile description enables a direct connection, so a description
|
||||
-- keeps its links only when both SimpleX links and direct messages are allowed.
|
||||
groupUserAllowSimplexLinks :: GroupInfo -> Bool
|
||||
groupUserAllowSimplexLinks g =
|
||||
groupFeatureUserAllowed SGFSimplexLinks g && groupFeatureUserAllowed SGFDirectMessages g
|
||||
|
||||
mergeUserChatPrefs :: User -> Contact -> FullPreferences
|
||||
mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct)
|
||||
|
||||
|
||||
@@ -2903,6 +2903,12 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil
|
||||
bob <## "bad chat command: feature not allowed SimpleX links"
|
||||
bob ##> ("/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}]")
|
||||
bob <## "bad chat command: feature not allowed SimpleX links"
|
||||
-- a link split with a space or a newline is still blocked
|
||||
let (lnk1, lnk2) = splitAt 12 inv
|
||||
bob ##> ("#team \"" <> lnk1 <> " " <> lnk2 <> "\"")
|
||||
bob <## "bad chat command: feature not allowed SimpleX links"
|
||||
bob ##> ("#team \"" <> lnk1 <> "\\n" <> lnk2 <> "\"")
|
||||
bob <## "bad chat command: feature not allowed SimpleX links"
|
||||
(alice </)
|
||||
(cath </)
|
||||
bob `send` ("@alice \"" <> inv <> "\\ntest\"")
|
||||
|
||||
@@ -25,6 +25,7 @@ markdownTests = do
|
||||
textColor
|
||||
textWithUri
|
||||
textWithHyperlink
|
||||
obfuscatedSimplexLinks
|
||||
textWithEmail
|
||||
textWithPhone
|
||||
textWithMentions
|
||||
@@ -284,6 +285,24 @@ textWithHyperlink = describe "text with HyperLink without link text" do
|
||||
"[click here](example.com)" <==> "[click here](example.com)"
|
||||
"[click here](https://example.com )" <==> "[click here](https://example.com )"
|
||||
|
||||
obfuscatedSimplexLinks :: Spec
|
||||
obfuscatedSimplexLinks = describe "SimpleX links obfuscated with whitespace" do
|
||||
let addr = "https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw"
|
||||
inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
let spaced s = T.replace "://" ":// " s -- insert a space right after the scheme
|
||||
it "detects links split with spaces or newlines" do
|
||||
hasObfuscatedSimplexLink addr `shouldBe` True
|
||||
hasObfuscatedSimplexLink (spaced addr) `shouldBe` True
|
||||
hasObfuscatedSimplexLink (T.intercalate "\n" $ T.chunksOf 8 addr) `shouldBe` True
|
||||
hasObfuscatedSimplexLink ("connect with me: " <> spaced addr) `shouldBe` True
|
||||
hasObfuscatedSimplexLink (T.intercalate " " $ T.chunksOf 8 $ "https://simplex.chat" <> inv) `shouldBe` True
|
||||
it "detects a split link followed by other text" do
|
||||
hasObfuscatedSimplexLink (spaced addr <> "\nplease connect") `shouldBe` True
|
||||
it "ignores text without a SimpleX link" do
|
||||
hasObfuscatedSimplexLink "" `shouldBe` False
|
||||
hasObfuscatedSimplexLink "hello there, this is a normal message" `shouldBe` False
|
||||
hasObfuscatedSimplexLink "see https://example.com/page?ref=123 for details" `shouldBe` False
|
||||
|
||||
email :: Text -> Markdown
|
||||
email = Markdown $ Just Email
|
||||
|
||||
|
||||
Reference in New Issue
Block a user