| index 8e25e71..b13f706 100644 |
| --- a/ios/LChatCore/Sources/LChatCore/ChatFeedModel.swift |
| +++ b/ios/LChatCore/Sources/LChatCore/ChatFeedModel.swift |
| @@ -162,8 +162,20 @@ public final class ChatFeedModel: ObservableObject { |
| private var lastHistoryLoad = Date.distantPast |
| |
| private let instance: SavedInstance |
| - /// The sender's screen name — the composer needs it to build a /me line. |
| - public var myScreenName: String { instance.screenName } |
| + /// The sender's screen name — stamped into every outgoing payload, and the |
| + /// composer's /me line. |
| + /// |
| + /// `instance.screenName` is THIS DEVICE's copy, and it is routinely empty: |
| + /// a space claimed from anything but an add-device link starts nameless |
| + /// (SpaceCreate.claim) and the setup conversation names us later — on |
| + /// whichever device answered, so a second device keeps its blank forever. |
| + /// Everything it stamped then read as "Someone" wherever a payload's own |
| + /// `name` is the only source (sidebar previews, push copy). So fall back to |
| + /// my roster label: the name every other member already sees. Web twin: |
| + /// stampName() in shell.js. |
| + public var myScreenName: String { |
| + instance.screenName.isEmpty ? (senderNames[myUserId] ?? "") : instance.screenName |
| + } |
| private let threadID: String |
| private let threadKind: String |
| private let threadKeyB64: String |
| @@ -599,7 +611,7 @@ public final class ChatFeedModel: ObservableObject { |
| when: Double? = nil, sid: String? = nil) -> Bool { |
| guard !text.isEmpty, key != nil, let socket, socket.isLive else { return false } |
| var payload = AppEventOut(type: type, id: id, text: text, |
| - name: instance.screenName, |
| + name: myScreenName, |
| ts: Int64(Date().timeIntervalSince1970 * 1000)) |
| if !myUserId.isEmpty { payload.uid = myUserId } |
| payload.when = when |
| @@ -643,7 +655,7 @@ public final class ChatFeedModel: ObservableObject { |
| let id = UUID().uuidString |
| var sid: String? = nil |
| var fire = RemindFireOut(id: id, notice: .init(title: "⏰ Reminder", body: text), |
| - name: instance.screenName, |
| + name: myScreenName, |
| ts: Int64(Date().timeIntervalSince1970 * 1000)) |
| if !myUserId.isEmpty { fire.uid = myUserId } |
| // The seconds conversion must never trap: Int64(Double) crashes on |
| @@ -747,7 +759,7 @@ public final class ChatFeedModel: ObservableObject { |
| guard threadKind == "group", !app.isEmpty, key != nil, |
| let socket, socket.isLive else { return false } |
| var payload = GroupAppsOut(type: add ? "groupapps.add" : "groupapps.remove", |
| - app: app, name: instance.screenName, |
| + app: app, name: myScreenName, |
| ts: Int64(Date().timeIntervalSince1970 * 1000)) |
| if !myUserId.isEmpty { payload.uid = myUserId } |
| encryptAndSend(payload, via: socket) |
| @@ -755,7 +767,7 @@ public final class ChatFeedModel: ObservableObject { |
| // right after the add, exactly like the web's installGroupApp — |
| // without it the app renders silently empty for every member. |
| if add, let ev = registry.manifest(app)?.openEvent, !ev.isEmpty { |
| - var open = OpenEventOut(type: ev, name: instance.screenName, |
| + var open = OpenEventOut(type: ev, name: myScreenName, |
| ts: Int64(Date().timeIntervalSince1970 * 1000)) |
| if !myUserId.isEmpty { open.uid = myUserId } |
| encryptAndSend(open, via: socket) |
| @@ -787,7 +799,7 @@ public final class ChatFeedModel: ObservableObject { |
| @discardableResult |
| public func sendAction(_ text: String) -> Bool { |
| guard !text.isEmpty, key != nil, let socket, socket.isLive else { return false } |
| - var payload = Outgoing(text: text, name: instance.screenName, |
| + var payload = Outgoing(text: text, name: myScreenName, |
| ts: Int64(Date().timeIntervalSince1970 * 1000)) |
| if !myUserId.isEmpty { payload.uid = myUserId } |
| payload.action = true |
| @@ -833,7 +845,7 @@ public final class ChatFeedModel: ObservableObject { |
| var quoted = m.raw |
| quoted.text = markdownPlainText(m.raw.text ?? "") |
| var payload = MessagePayload(type: "reaction", |
| - name: instance.screenName, |
| + name: myScreenName, |
| ts: Int64(Date().timeIntervalSince1970 * 1000), |
| targetSid: id, |
| targetUid: m.raw.uid, |
| @@ -891,7 +903,7 @@ public final class ChatFeedModel: ObservableObject { |
| guard !trimmed.isEmpty || !attachments.isEmpty, key != nil, |
| let socket, socket.isLive else { return false } |
| let ts = Int64(Date().timeIntervalSince1970 * 1000) |
| - var payload = Outgoing(text: trimmed, name: instance.screenName, ts: ts) |
| + var payload = Outgoing(text: trimmed, name: myScreenName, ts: ts) |
| if !myUserId.isEmpty { payload.uid = myUserId } |
| if !attachments.isEmpty { payload.attachments = attachments } |
| payload.replyTo = replyTo |
| @@ -916,7 +928,7 @@ public final class ChatFeedModel: ObservableObject { |
| public func sendViaREST(_ text: String, attachments: [Attachment]) async -> Bool { |
| guard let key, let base = instance.baseURL, |
| !text.isEmpty || !attachments.isEmpty else { return false } |
| - var payload = Outgoing(text: emojify(text), name: instance.screenName, |
| + var payload = Outgoing(text: emojify(text), name: myScreenName, |
| ts: Int64(Date().timeIntervalSince1970 * 1000)) |
| if !myUserId.isEmpty { payload.uid = myUserId } |
| if !attachments.isEmpty { payload.attachments = attachments } |
| @@ -1024,7 +1036,7 @@ public final class ChatFeedModel: ObservableObject { |
| guard s.payload.isRenderableChat else { return nil } |
| let p = s.payload |
| let mine = p.uid.map { !myUserId.isEmpty && $0 == myUserId } |
| - ?? (p.name == instance.screenName) |
| + ?? (!myScreenName.isEmpty && p.name == myScreenName) |
| return Message(id: s.id, name: p.name ?? "", text: p.text ?? "", |
| mine: mine, at: s.at, action: p.action ?? false, |
| edited: s.edited, attachments: p.attachments ?? [], |
| @@ -1091,7 +1103,7 @@ public final class ChatFeedModel: ObservableObject { |
| } |
| } |
| let myUserId = self.myUserId |
| - let screenName = self.instance.screenName |
| + let screenName = self.myScreenName |
| let wantGroupApps = threadKind == "group" |
| let foldSpan = Perf.begin("feed.foldPage", slowMs: 250) |
| let (fresh, stored, appEvents, reactionRows, unprojectedRows): |
| @@ -1134,7 +1146,7 @@ public final class ChatFeedModel: ObservableObject { |
| reactions: [], payload: p)) |
| guard p.isRenderableChat else { continue } |
| let mine = p.uid.map { !myUserId.isEmpty && $0 == myUserId } |
| - ?? (p.name == screenName) |
| + ?? (!screenName.isEmpty && p.name == screenName) |
| out.append(Message(id: row.id, name: p.name ?? "", text: p.text ?? "", |
| mine: mine, at: row.created_at, |
| action: p.action ?? false, |
| @@ -1369,7 +1381,7 @@ public final class ChatFeedModel: ObservableObject { |
| guard p.type == "chat.typing", let name = p.name, !name.isEmpty else { return } |
| if let uid = p.uid, !myUserId.isEmpty { |
| if uid == myUserId { return } |
| - } else if name == instance.screenName { |
| + } else if !myScreenName.isEmpty, name == myScreenName { |
| return |
| } |
| let secs = min(max(p.expiry ?? 0, 0), 60) |
| @@ -1429,7 +1441,7 @@ public final class ChatFeedModel: ObservableObject { |
| |
| private func sendTypingBeat(expiry: Int) { |
| guard let key, let socket else { return } |
| - var payload = TypingOut(name: instance.screenName, |
| + var payload = TypingOut(name: myScreenName, |
| ts: Int64(Date().timeIntervalSince1970 * 1000), expiry: expiry) |
| if !myUserId.isEmpty { payload.uid = myUserId } |
| lastBeatSent = Date() |
| @@ -1582,7 +1594,7 @@ public final class ChatFeedModel: ObservableObject { |
| // Own-message check mirrors the web (ownMessage in ui.js): uid when the |
| // payload carries one, else the screen name (old payloads). |
| let mine = p.uid.map { !myUserId.isEmpty && $0 == myUserId } |
| - ?? (p.name == instance.screenName) |
| + ?? (!myScreenName.isEmpty && p.name == myScreenName) |
| // The client id to pair on: the explicit optimistic-fold argument, else |
| // the one riding in the echo's payload. A confirming echo has pending == |
| // false and reads it from `p._cid`; a fresh optimistic copy passes it in. |
| @@ -1658,7 +1670,7 @@ public final class ChatFeedModel: ObservableObject { |
| pendingIDSeq &+= 1 |
| var p = MessagePayload() |
| p.text = text |
| - p.name = instance.screenName |
| + p.name = myScreenName |
| p.ts = ts |
| if !myUserId.isEmpty { p.uid = myUserId } |
| if !attachments.isEmpty { p.attachments = attachments } |