Merge branch 'master' into chat-relays

This commit is contained in:
spaced4ndy
2026-03-06 19:27:07 +04:00
19 changed files with 500 additions and 63 deletions
@@ -512,7 +512,9 @@ struct GroupChatInfoView: View {
}
private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey {
if member.activeConn?.connDisabled ?? false {
if case .failed = member.activeConn?.connStatus {
return "failed"
} else if member.activeConn?.connDisabled ?? false {
return "disabled"
} else if member.activeConn?.connInactive ?? false {
return "inactive"
@@ -234,6 +234,12 @@ struct GroupMemberInfoView: View {
}
}
if let connFailedErr = member.activeConn?.connFailedErr {
Section {
infoRow("Connection failed", connFailedErr)
}
}
if groupInfo.membership.memberRole >= .moderator {
adminDestructiveSection(member)
} else if !groupInfo.useRelays {
@@ -196,7 +196,9 @@ struct MemberSupportView: View {
}
private func memberStatus(_ member: GroupMember) -> LocalizedStringKey {
if member.activeConn?.connDisabled ?? false {
if case .failed = member.activeConn?.connStatus {
return "failed"
} else if member.activeConn?.connDisabled ?? false {
return "disabled"
} else if member.activeConn?.connInactive ?? false {
return "inactive"
+16 -9
View File
@@ -2117,6 +2117,11 @@ public struct Connection: Decodable, Hashable {
public var id: ChatId { get { ":\(connId)" } }
public var connFailedErr: String? {
if case let .failed(err) = connStatus { return err }
return nil
}
public var connDisabled: Bool {
authErrCounter >= 10 // authErrDisableCount in core
}
@@ -2302,15 +2307,16 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable {
}
}
public enum ConnStatus: String, Decodable, Hashable {
case new = "new"
case prepared = "prepared"
case joined = "joined"
case requested = "requested"
case accepted = "accepted"
case sndReady = "snd-ready"
case ready = "ready"
case deleted = "deleted"
public enum ConnStatus: Decodable, Hashable {
case new
case prepared
case joined
case requested
case accepted
case sndReady
case ready
case deleted
case failed(connError: String)
var initiated: Bool? {
get {
@@ -2323,6 +2329,7 @@ public enum ConnStatus: String, Decodable, Hashable {
case .sndReady: return nil
case .ready: return nil
case .deleted: return nil
case .failed: return nil
}
}
}
@@ -1904,6 +1904,12 @@ data class Connection(
val connInactive: Boolean
get() = quotaErrCounter >= 5 // quotaErrInactiveCount in core
val connFailedErr: String?
get() = when (connStatus) {
is ConnStatus.Failed -> connStatus.connError
else -> null
}
val connPQEnabled: Boolean
get() = pqSndEnabled == true && pqRcvEnabled == true
@@ -2638,25 +2644,27 @@ class PendingContactConnection(
}
@Serializable
enum class ConnStatus {
@SerialName("new") New,
@SerialName("prepared") Prepared,
@SerialName("joined") Joined,
@SerialName("requested") Requested,
@SerialName("accepted") Accepted,
@SerialName("snd-ready") SndReady,
@SerialName("ready") Ready,
@SerialName("deleted") Deleted;
sealed class ConnStatus {
@Serializable @SerialName("new") object New: ConnStatus()
@Serializable @SerialName("prepared") object Prepared: ConnStatus()
@Serializable @SerialName("joined") object Joined: ConnStatus()
@Serializable @SerialName("requested") object Requested: ConnStatus()
@Serializable @SerialName("accepted") object Accepted: ConnStatus()
@Serializable @SerialName("sndReady") object SndReady: ConnStatus()
@Serializable @SerialName("ready") object Ready: ConnStatus()
@Serializable @SerialName("deleted") object Deleted: ConnStatus()
@Serializable @SerialName("failed") class Failed(val connError: String): ConnStatus()
val initiated: Boolean? get() = when (this) {
New -> true
Prepared -> false
Joined -> false
Requested -> true
Accepted -> true
SndReady -> null
Ready -> null
Deleted -> null
is New -> true
is Prepared -> false
is Joined -> false
is Requested -> true
is Accepted -> true
is SndReady -> null
is Ready -> null
is Deleted -> null
is Failed -> null
}
}
@@ -878,9 +878,11 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr
}
fun memberConnStatus(): String {
return if (member.activeConn?.connDisabled == true) {
generalGetString(MR.strings.member_info_member_disabled)
return if (member.activeConn?.connStatus is ConnStatus.Failed) {
generalGetString(MR.strings.member_info_member_failed)
} else if (member.activeConn?.connDisabled == true) {
generalGetString(MR.strings.member_info_member_disabled)
} else if (member.activeConn?.connInactive == true) {
generalGetString(MR.strings.member_info_member_inactive)
} else {
member.memberStatus.shortText
@@ -562,6 +562,14 @@ fun GroupMemberInfoLayout(
}
}
val connFailedErr = member.activeConn?.connFailedErr
if (connFailedErr != null) {
SectionDividerSpaced()
SectionView {
InfoRow(stringResource(MR.strings.info_row_connection_failed), connFailedErr)
}
}
if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
ModeratorDestructiveSection()
} else {
@@ -162,7 +162,9 @@ private fun ModalData.MemberSupportViewLayout(
@Composable
fun SupportChatRow(member: GroupMember) {
fun memberStatus(): String {
return if (member.activeConn?.connDisabled == true) {
return if (member.activeConn?.connStatus is ConnStatus.Failed) {
generalGetString(MR.strings.member_info_member_failed)
} else if (member.activeConn?.connDisabled == true) {
generalGetString(MR.strings.member_info_member_disabled)
} else if (member.activeConn?.connInactive == true) {
generalGetString(MR.strings.member_info_member_inactive)
@@ -1906,6 +1906,7 @@
<string name="member_blocked_by_admin">Blocked by admin</string>
<string name="member_info_member_blocked">blocked</string>
<string name="member_info_member_disabled">disabled</string>
<string name="member_info_member_failed">failed</string>
<string name="member_info_member_inactive">inactive</string>
<string name="member_info_section_title_member">MEMBER</string>
<string name="role_in_group">Role</string>
@@ -1924,6 +1925,7 @@
<string name="info_row_group">Group</string>
<string name="info_row_chat">Chat</string>
<string name="info_row_connection">Connection</string>
<string name="info_row_connection_failed">Connection failed</string>
<string name="conn_level_desc_direct">direct</string>
<string name="conn_level_desc_indirect">indirect (%1$s)</string>
<string name="message_queue_info">Message queue info</string>
+29 -9
View File
@@ -1485,15 +1485,35 @@ LARGE:
## ConnStatus
**Enum type**:
- "new"
- "prepared"
- "joined"
- "requested"
- "accepted"
- "snd-ready"
- "ready"
- "deleted"
**Discriminated union type**:
New:
- type: "new"
Prepared:
- type: "prepared"
Joined:
- type: "joined"
Requested:
- type: "requested"
Accepted:
- type: "accepted"
SndReady:
- type: "sndReady"
Ready:
- type: "ready"
Deleted:
- type: "deleted"
Failed:
- type: "failed"
- connError: string
---
+1 -1
View File
@@ -243,7 +243,7 @@ chatTypesDocsData =
(sti @ConnectionErrorType, STUnion, "", [], "", ""),
(sti @ConnectionMode, (STEnum' $ take 3 . consLower "CM"), "", [], "", ""),
(sti @ConnectionPlan, STUnion, "CP", [], "", ""),
(sti @ConnStatus, (STEnum' $ consSep "Conn" '-'), "", [], "", ""),
(sti @ConnStatus, STUnion, "Conn", [], "", ""),
(sti @ConnType, (STEnum' $ consSep "Conn" '_'), "", [], "", ""),
(sti @Contact, STRecord, "", [], "", ""),
(sti @ContactAddressPlan, STUnion, "CAP", [], "", ""),
@@ -1713,15 +1713,69 @@ export interface ComposedMessage {
mentions: {[key: string]: number} // string : int64
}
export enum ConnStatus {
New = "new",
Prepared = "prepared",
Joined = "joined",
Requested = "requested",
Accepted = "accepted",
Snd_ready = "snd-ready",
Ready = "ready",
Deleted = "deleted",
export type ConnStatus =
| ConnStatus.New
| ConnStatus.Prepared
| ConnStatus.Joined
| ConnStatus.Requested
| ConnStatus.Accepted
| ConnStatus.SndReady
| ConnStatus.Ready
| ConnStatus.Deleted
| ConnStatus.Failed
export namespace ConnStatus {
export type Tag =
| "new"
| "prepared"
| "joined"
| "requested"
| "accepted"
| "sndReady"
| "ready"
| "deleted"
| "failed"
interface Interface {
type: Tag
}
export interface New extends Interface {
type: "new"
}
export interface Prepared extends Interface {
type: "prepared"
}
export interface Joined extends Interface {
type: "joined"
}
export interface Requested extends Interface {
type: "requested"
}
export interface Accepted extends Interface {
type: "accepted"
}
export interface SndReady extends Interface {
type: "sndReady"
}
export interface Ready extends Interface {
type: "ready"
}
export interface Deleted extends Interface {
type: "deleted"
}
export interface Failed extends Interface {
type: "failed"
connError: string
}
}
export enum ConnType {
+316
View File
@@ -0,0 +1,316 @@
# Save Permanent Connection Errors for Group Members
## Context
When a group member's connection handshake fails with a permanent error (e.g., `CONN NOT_ACCEPTED`, `SMP AUTH`, `AGENT A_VERSION`), the ERR event is logged to the UI event stream and discarded. The member record stays stuck in a "connecting" `GroupMemberStatus` (like `memIntroduced`, `memAccepted`) forever. Users see perpetual "connecting" with no explanation and no way to know whether to wait or re-invite.
**Root cause**: `agentMsgConnStatus` (Subscriber.hs:376) only maps success events (`CONF`, `INFO`, `JOINED`, `CON`) to status transitions. The ERR handler for group members (Subscriber.hs:1054-1056) only logs to UI and completes the command — no status or error is persisted.
## Solution Summary
Add `ConnError {connError :: Text}` constructor to `ConnStatus`. Error text is encoded in the `conn_status TEXT` column as `"error <text>"` via `TextEncoding`, and in JSON via `sumTypeJSON` (following `GSSError`/`CIFileStatus` pattern). No new DB column, no migration. When a non-temporary ERR arrives before connection is ready, transition to `ConnError` and notify UI. Messages are not queued for errored connections.
## Technical Design
### Error classification
Use `temporaryOrHostError` from `Simplex.Messaging.Agent.Client` (simplexmq Client.hs:1486, exported at line 60):
- Returns `True` for NETWORK, TIMEOUT, HOST, TEVersion, INACTIVE, CRITICAL-with-restart → **do not save**
- Returns `False` for AUTH, CONN errors, VERSION, INTERNAL, etc. → **save as permanent error**
Guard: only save when connection is not `ConnReady` and not already `ConnError`. Post-handshake errors (when `connStatus == ConnReady`) are handled by existing `processConnMERR` (AUTH counters, QUOTA counters).
### Data flow
```
Agent ERR event
→ Subscriber.hs processGroupMessage ERR handler
→ guard: connStatus is not ConnReady, not ConnError, not temporaryOrHostError
→ DB: UPDATE connections SET conn_status = 'error <tshow err>'
→ emit: CEvtGroupMemberUpdated user gInfo m m'
→ iOS: upsertGroupMember updates model → UI re-renders
```
### DB encoding
`conn_status TEXT NOT NULL` already exists. `ConnError` encodes as `"error " <> errText` using `TextEncoding` (same as `GSSError`). No migration needed — new text values are valid in the existing column.
### JSON encoding
Replace manual `ToJSON`/`FromJSON` instances with `$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Conn") ''ConnStatus)`. This follows the `GroupSndStatus`/`CIFileStatus` pattern — `sumTypeJSON` is already imported in Types.hs (line 60).
JSON format (platform-dependent via `sumTypeJSON`):
- iOS: `{"error": {"connError": "SMP AUTH"}}` (ObjectWithSingleField)
- Android/Desktop: `{"type": "error", "connError": "SMP AUTH"}` (TaggedObject)
- Nullary cases: `{"ready": {}}` / `{"type": "ready"}` (not plain `"ready"` strings)
Note: `ConnSndReady` JSON tag changes from `"snd-ready"` to `"sndReady"` (`dropPrefix "Conn"` applies `fstToLower`). This is safe — JSON is core→UI within same build. Swift auto-synthesis matches on `sndReady` case name.
### Clear on recovery
When CON event arrives, `agentMsgConnStatus` returns `Just ConnReady`, `updateConnStatus` overwrites `conn_status` to `"ready"`. Error is implicitly cleared — no special cleanup needed.
### ConnStatus state machine update
```
Existing transitions (unchanged):
ConnNew → ConnRequested → ConnAccepted → ConnSndReady → ConnReady
ConnNew → ConnJoined → ConnSndReady → ConnReady
ConnPrepared → ConnJoined → ConnSndReady → ConnReady
Any → ConnDeleted
New transitions:
Any pre-ready state → ConnError (on permanent ERR)
ConnError → ConnReady (on successful CON — recovery)
ConnError → ConnDeleted (on connection deletion)
```
### Pattern match safety audit
Traced every ConnStatus pattern match across Haskell (10 files), Swift (6 files), Kotlin (3 files).
**Must update (exhaustive matches):**
| Location | Change |
|---|---|
| Types.hs textEncode/textDecode (~1703) | Add ConnError encoding/decoding |
| Types.hs ToJSON/FromJSON (~1696) | Replace with `sumTypeJSON` TH splice |
| Swift ConnStatus.initiated | Add `case .error: return nil` |
| Kotlin ConnStatus.initiated | Add `Error -> null` (follow-up) |
**Must update (behavioral):**
| Location | Current behavior | Fix |
|---|---|---|
| Internal.hs memberSendAction (line 2041) | ConnError falls to `otherwise -> pendingOrForwarded` — messages queued for permanently errored connections | Add pattern guard `ConnError {} <- connStatus -> Nothing` |
**Verified safe — no changes needed:**
| Pattern | Sites | Why safe |
|---|---|---|
| `== ConnReady` / `== ConnSndReady` | 12 sites (connReady, Contact.ready, GroupMember.ready, sndReady, readyMemberConn, xftpSndFileTransfer) | ConnError ≠ these → excluded from "ready" paths |
| `== ConnPrepared` | 8 sites (joinPreparedConn, nextConnectPrepared, isContactCard, contactRequestPlan) | ConnError ≠ ConnPrepared → doesn't trigger join/prepare logic |
| `== ConnNew` | 4 sites (contactConnInitiated, nextAcceptContactRequest, APIPrepareContact) | ConnError ≠ ConnNew → doesn't trigger new-connection logic |
| `!= ConnDeleted` (DB WHERE) | 6 sites (getConnectionEntity, *ConnsToSub) | ConnError ≠ ConnDeleted → errored connections remain findable and subscribable (correct — enables recovery via CON). **Add TODO comments** at each site to consider whether ConnError connections should be excluded. |
| `updateConnectionStatusFromTo` | 3 sites | Compares current to specific `fromStatus` — ConnError won't accidentally match |
| `readyMemberConn` (Internal.hs:2078) | 1 site | `connStatus == ConnReady \|\| == ConnSndReady` — ConnError → `otherwise = Nothing` (correct) |
| `connDisabled`/`connInactive` | 6 sites | Derived from error counters, not connStatus |
| `agentMsgConnStatus` | 1 site | Only produces ConnSndReady/ConnRequested/ConnReady — no ConnError output |
## Implementation Plan
### 1. Haskell: ConnStatus type
**File: `src/Simplex/Chat/Types.hs`**
**ConnStatus** (~line 1673): Add constructor after `ConnDeleted`:
```haskell
| ConnError {connError :: Text}
```
Record syntax for `sumTypeJSON` field name in JSON. `deriving (Eq, Show, Read)` unchanged.
**TextEncoding instance** (~line 1703) — for DB storage:
```haskell
textEncode = \case
...
ConnError err -> "error " <> err
textDecode s
| Just err <- T.stripPrefix "error " s = Just (ConnError err)
| otherwise = case s of
"new" -> Just ConnNew
... (existing cases unchanged)
_ -> Nothing
```
Note: `textDecode` changes from `\case` to named parameter `s` to support `stripPrefix` guard.
**JSON instances** (~lines 1696-1701): Replace manual instances with TH splice:
```haskell
-- Remove:
-- instance FromJSON ConnStatus where parseJSON = textParseJSON "ConnStatus"
-- instance ToJSON ConnStatus where toJSON = J.String . textEncode; toEncoding = JE.text . textEncode
-- Add:
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Conn") ''ConnStatus)
```
`sumTypeJSON` and `dropPrefix` already imported (line 60). `FromField`/`ToField` instances unchanged — still use `TextEncoding` for DB.
**`connReady`** (line 1597): No change — `== ConnReady || == ConnSndReady`, `ConnError _` naturally returns `False`.
### 2. Haskell: Subscriber.hs — save error on permanent ERR
**File: `src/Simplex/Chat/Library/Subscriber.hs`**
Extend existing import (line 74):
```haskell
import Simplex.Messaging.Agent.Client (temporaryOrHostError, getAgentWorker, ...)
```
Update ERR handler in `processGroupMessage` (line 1054-1056). Current:
```haskell
ERR err -> do
eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
```
New:
```haskell
ERR err -> do
eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
let Connection {connStatus = cs} = conn
case cs of
ConnReady -> pure ()
ConnError _ -> pure ()
_ | temporaryOrHostError err -> pure ()
| otherwise -> do
let errText = tshow err
withStore' $ \db -> updateConnectionStatus db conn (ConnError errText)
let conn' = conn {connStatus = ConnError errText}
m' = m {activeConn = Just conn'}
toView $ CEvtGroupMemberUpdated user gInfo m m'
```
Note: `let Connection {connStatus = cs} = conn` destructures via pattern binding, avoiding ambiguous `connStatus conn` field selector under `DuplicateRecordFields`.
No new store function — reuses existing `updateConnectionStatus` (Direct.hs:937) which calls `updateConnectionStatus_``textEncode` → stores `"error SMP AUTH"` in `conn_status`.
### 3. Haskell: memberSendAction — don't queue for errored connections
**File: `src/Simplex/Chat/Library/Internal.hs`**
Update `memberSendAction` (line 2040-2044). Current:
```haskell
Just conn@Connection {connStatus}
| connDisabled conn || connStatus == ConnDeleted || memberStatus == GSMemRejected -> Nothing
| connInactive conn -> Just MSAPending
| connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn
| otherwise -> pendingOrForwarded
```
Add pattern guard after first guard (can't use `==` with associated data):
```haskell
Just conn@Connection {connStatus}
| connDisabled conn || connStatus == ConnDeleted || memberStatus == GSMemRejected -> Nothing
| ConnError {} <- connStatus -> Nothing
| connInactive conn -> Just MSAPending
| connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn
| otherwise -> pendingOrForwarded
```
### 4. Swift: ConnStatus enum
**File: `apps/ios/SimpleXChat/ChatTypes.swift`** (~line 2301)
Change from `String`-backed raw value enum to enum with associated value. Auto-synthesized `Decodable` handles `sumTypeJSON` format (same as `GroupSndStatus`, `CIFileStatus`):
```swift
public enum ConnStatus: Decodable, Hashable {
case new
case prepared
case joined
case requested
case accepted
case sndReady
case ready
case deleted
case error(connError: String)
var initiated: Bool? {
switch self {
case .new: return true
case .prepared: return false
case .joined: return false
case .requested: return true
case .accepted: return true
case .sndReady: return nil
case .ready: return nil
case .deleted: return nil
case .error: return nil
}
}
}
```
No custom `init(from:)` needed. `Hashable`/`Equatable` auto-synthesized. Existing equality checks like `connStatus == .ready` still compile (nullary cases).
### 5. Swift: Connection computed property
**File: `apps/ios/SimpleXChat/ChatTypes.swift`**
Add computed property to `Connection` struct (~line 2092, after `connStatus`):
```swift
public var connError: String? {
if case let .error(err) = connStatus { return err }
return nil
}
```
### 6. Swift: Member list status
**File: `apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift`**
Update `memberConnStatus` function (~line 457). Insert error check FIRST (before `connDisabled`/`connInactive`):
```swift
private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey {
if case .error = member.activeConn?.connStatus {
return "connection error"
} else if member.activeConn?.connDisabled ?? false {
return "disabled"
} else if member.activeConn?.connInactive ?? false {
return "inactive"
} else {
return member.memberStatus.shortText
}
}
```
**File: `apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift`**
Update `memberStatus` function (line 198). Insert error check FIRST (before `connDisabled` at line 199):
```swift
if case .error = member.activeConn?.connStatus {
return "connection error"
} else if member.activeConn?.connDisabled ?? false {
```
### 7. Swift: Member info error display
**File: `apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift`**
Add error display section after the `connStats` section (~line 190):
```swift
if let connError = member.activeConn?.connError {
Section(header: Text("Connection error").foregroundColor(theme.colors.secondary)) {
Text(connError)
.foregroundColor(theme.colors.secondary)
.font(.callout)
.textSelection(.enabled)
}
}
```
## Files Changed Summary
| Layer | File | Change |
|-------|------|--------|
| Core | `Types.hs` | Add `ConnError {connError :: Text}` to ConnStatus, update TextEncoding, replace JSON with `sumTypeJSON` TH splice |
| Logic | `Subscriber.hs` | Import `temporaryOrHostError`, handle permanent ERR for group members |
| Logic | `Internal.hs` | Add `ConnError` guard to `memberSendAction` → return `Nothing` |
| iOS | `ChatTypes.swift` | ConnStatus: auto-synthesized Decodable with `.error(connError:)`, Connection: `connError` computed property |
| iOS | `GroupChatInfoView.swift` | Show "connection error" in `memberConnStatus` (first check) |
| iOS | `MemberSupportView.swift` | Show "connection error" in `memberStatus` (first check) |
| iOS | `GroupMemberInfoView.swift` | Show error description section |
## Verification
1. **Build Haskell**: `cabal build --ghc-options -O0`
2. **Build iOS**: Verify Swift compiles — existing `connStatus == .ready` comparisons still work (nullary cases)
3. **JSON format**: Verify `sumTypeJSON` output matches Swift auto-synthesis expectations (nullary: `{"ready": {}}`, error: `{"error": {"connError": "..."}}`)
4. **Backward compat**: New `"error ..."` values in `conn_status` only appear after code update. Old code cannot parse them (downgrade risk, same as any new enum value).
5. **Recovery**: CON event → `updateConnectionStatus_ ConnReady` → overwrites `"error ..."` with `"ready"` in DB
6. **memberSendAction**: Verify messages are NOT queued for ConnError connections
## Out of Scope (immediate follow-up)
**Kotlin/Android/Desktop**: `ConnStatus` enum in `ChatModel.kt:2640` needs custom serializer for `sumTypeJSON` format (TaggedObject: `{"type": "error", "connError": "..."}`) + `Connection` needs `connError` computed property + member status UI. Must be updated before Android/Desktop builds from this commit. Existing bug at `GroupChatInfoView.kt:883` (`connDisabled` checked twice, should be `connInactive` on second check).
+1 -1
View File
@@ -2129,7 +2129,7 @@ memberSendAction gInfo@GroupInfo {membership} events members m@GroupMember {memb
| otherwise = case memberConn m of
Nothing -> pendingOrForwarded
Just conn@Connection {connStatus}
| connDisabled conn || connStatus == ConnDeleted || memberStatus == GSMemRejected -> Nothing
| connDisabled conn || connStatus == ConnDeleted || isConnFailed connStatus || memberStatus == GSMemRejected -> Nothing
| connInactive conn -> Just MSAPending
| connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn
| otherwise -> pendingOrForwarded
+6 -3
View File
@@ -370,19 +370,20 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
processContactConnMessage agentMessage entity conn uc
where
updateConnStatus :: ConnectionEntity -> CM ConnectionEntity
updateConnStatus acEntity = case agentMsgConnStatus agentMessage of
updateConnStatus acEntity = case agentMsgConnStatus (entityConnection acEntity) agentMessage of
Just connStatus -> do
let conn = (entityConnection acEntity) {connStatus}
withStore' $ \db -> updateConnectionStatus db conn connStatus
pure $ updateEntityConnStatus acEntity connStatus
Nothing -> pure acEntity
agentMsgConnStatus :: AEvent e -> Maybe ConnStatus
agentMsgConnStatus = \case
agentMsgConnStatus :: Connection -> AEvent e -> Maybe ConnStatus
agentMsgConnStatus Connection {connStatus = cs} = \case
JOINED True _ -> Just ConnSndReady
CONF {} -> Just ConnRequested
INFO {} -> Just ConnSndReady
CON _ -> Just ConnReady
ERR err | cs /= ConnReady && not (temporaryOrHostError err) -> Just $ ConnFailed (tshow err)
_ -> Nothing
processCONFpqSupport :: Connection -> PQSupport -> CM Connection
@@ -1121,6 +1122,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ERR err -> do
eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
when (isConnFailed $ connStatus conn) $
toView $ CEvtGroupMemberUpdated user gInfo m m
-- TODO add debugging output
_ -> pure ()
where
+3
View File
@@ -71,6 +71,9 @@ getChatLockEntity db agentConnId = do
ExceptT . firstRow fromOnly (SEInternalError "group member connection group_id not found") $
DB.query db "SELECT group_id FROM group_members WHERE group_member_id = ?" (Only groupMemberId)
-- TODO consider whether ConnFailed connections should be excluded:
-- - from receiving: getConnectionEntity, getContactConnEntityByConnReqHash
-- - from subscribing: getContactConnsToSub, getUCLConnsToSub, getMemberConnsToSub, getPendingConnsToSub
getConnectionEntity :: DB.Connection -> VersionRangeChat -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity
getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
c@Connection {connType, entityId} <- getConnection_
+11 -7
View File
@@ -1741,19 +1741,14 @@ data ConnStatus
ConnReady
| -- | connection deleted
ConnDeleted
| -- | connection had a permanent error during handshake
ConnFailed {connError :: Text}
deriving (Eq, Show, Read)
instance FromField ConnStatus where fromField = fromTextField_ textDecode
instance ToField ConnStatus where toField = toField . textEncode
instance FromJSON ConnStatus where
parseJSON = textParseJSON "ConnStatus"
instance ToJSON ConnStatus where
toJSON = J.String . textEncode
toEncoding = JE.text . textEncode
instance TextEncoding ConnStatus where
textDecode = \case
"new" -> Just ConnNew
@@ -1764,6 +1759,7 @@ instance TextEncoding ConnStatus where
"snd-ready" -> Just ConnSndReady
"ready" -> Just ConnReady
"deleted" -> Just ConnDeleted
s | Just err <- T.stripPrefix "failed " s -> Just (ConnFailed err)
_ -> Nothing
textEncode = \case
ConnNew -> "new"
@@ -1774,6 +1770,12 @@ instance TextEncoding ConnStatus where
ConnSndReady -> "snd-ready"
ConnReady -> "ready"
ConnDeleted -> "deleted"
ConnFailed err -> "failed " <> err
isConnFailed :: ConnStatus -> Bool
isConnFailed = \case
ConnFailed {} -> True
_ -> False
data ConnType = ConnContact | ConnMember | ConnUserContact
deriving (Eq, Show)
@@ -1997,6 +1999,8 @@ $(JQ.deriveJSON defaultJSON ''GroupMemberSettings)
$(JQ.deriveJSON defaultJSON ''SecurityCode)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Conn") ''ConnStatus)
$(JQ.deriveJSON defaultJSON ''Connection)
$(JQ.deriveJSON defaultJSON ''PendingContactConnection)
-1
View File
@@ -260,7 +260,6 @@ active_directory: true
app</a>.</p>
<p>SimpleX Directory is also available as a <a href="https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok" target="_blank">SimpleX chat bot</a>.</p>
<p>Read about <a href="/docs/directory.html">how to add</a> your community</a>.</p>
<p style="color:red;">Under maintenance &mdash; you can't join these groups until <a href="https://time.is/UTC" target="_blank" style="color:red;">17:00 UTC</a>, 01/09.</p>
<div class="search-container">
<input id="search" autocomplete="off">
<div id="top-pagination" class="pagination">
-1
View File
@@ -223,7 +223,6 @@ active_home: true
<p>{{ "index-directory-p1" | i18n({}, lang) }}</p>
<p>{{ "index-directory-p2" | i18n({}, lang) }}</p>
<a class="gradient-text" href="/directory">{{ "index-directory-cta" | i18n({}, lang) }}</a>
<p class="gradient-text">Under maintenance &mdash; you can't join these groups until <a href="https://time.is/UTC" target="_blank">17:00 UTC</a>, 01/09.</p>
</div>
</div>
</section>