Skip to content

Commit 4b943dc

Browse files
steipeteomarshahine
andcommitted
fix(poll): restore vote payload delivery
Co-authored-by: Omar Shahine <[email protected]>
1 parent 9452489 commit 4b943dc

4 files changed

Lines changed: 32 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 0.12.2 - Unreleased
44

5+
### Native Polls
6+
- fix: restore native poll vote delivery on macOS 26.4 by persisting the Polls balloon and payload across the responding message objects (#150, thanks @omarshahine).
7+
58
## 0.12.1 - 2026-07-02
69

710
### Packaging

Sources/IMsgHelper/IMsgInjected.m

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2815,15 +2815,9 @@ static id buildPollVoteIMMessage(NSAttributedString *body,
28152815
Class messageClass = NSClassFromString(@"IMMessage");
28162816
if (!messageClass) return nil;
28172817

2818-
// No macOS 26 IMMessage initializer carries balloon + payload AND
2819-
// association together (verified by selector probe). Reactions prove the
2820-
// associated-message initializer persists association atomically, and
2821-
// balloon/payload set on the backing IMMessageItem persist (verified: an
2822-
// earlier item-first vote landed with the balloon intact, only the
2823-
// association — set via the wrap — was lost). So: init with association
2824-
// atomically, then stamp balloonBundleID + payloadData onto the message's
2825-
// own backing item (a real item from a direct init, not the transient one
2826-
// the messageFromIMMessageItem: wrap returns).
2818+
// No macOS 26 initializer carries the payload and association together.
2819+
// Build the association atomically, then stamp payload metadata across the
2820+
// message and backing item; macOS 26.4 splits the setters between them.
28272821
SEL sel = @selector(initWithSender:time:text:messageSubject:fileTransferGUIDs:flags:error:guid:subject:associatedMessageGUID:associatedMessageType:associatedMessageRange:messageSummaryInfo:);
28282822
if (![messageClass instancesRespondToSelector:sel]) return nil;
28292823

@@ -2857,8 +2851,6 @@ static id buildPollVoteIMMessage(NSAttributedString *body,
28572851
id result = invokeReturningObject(inv);
28582852
if (!result) return nil;
28592853

2860-
// Both values must land on one object. A partial stamp can emit an
2861-
// associated message that Messages cannot decode as a poll vote.
28622854
NSString *balloonID = pollsBalloonBundleIdentifier();
28632855
NSArray<id> *targets = @[result];
28642856
SEL itemSel = NSSelectorFromString(@"_imMessageItem");
@@ -2868,14 +2860,19 @@ static id buildPollVoteIMMessage(NSAttributedString *body,
28682860
if (item) targets = @[item, result];
28692861
} @catch (__unused NSException *e) {}
28702862
}
2863+
BOOL balloonStamped = NO;
2864+
BOOL payloadStamped = NO;
28712865
for (id target in targets) {
2872-
if (![target respondsToSelector:@selector(setBalloonBundleID:)]
2873-
|| ![target respondsToSelector:@selector(setPayloadData:)]) continue;
2874-
[target performSelector:@selector(setBalloonBundleID:) withObject:balloonID];
2875-
[target performSelector:@selector(setPayloadData:) withObject:payloadData];
2876-
return result;
2866+
if ([target respondsToSelector:@selector(setBalloonBundleID:)]) {
2867+
[target performSelector:@selector(setBalloonBundleID:) withObject:balloonID];
2868+
balloonStamped = YES;
2869+
}
2870+
if ([target respondsToSelector:@selector(setPayloadData:)]) {
2871+
[target performSelector:@selector(setPayloadData:) withObject:payloadData];
2872+
payloadStamped = YES;
2873+
}
28772874
}
2878-
return nil;
2875+
return (balloonStamped && payloadStamped) ? result : nil;
28792876
}
28802877

28812878
/// `send-poll-vote`: cast a vote on an existing poll. Builds a Polls balloon

Tests/imsgTests/BridgeCommandRegistrationTests.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func injectedHelperWiresNativePollSend() throws {
150150
}
151151

152152
@Test
153-
func injectedHelperWiresFailClosedNativePollVote() throws {
153+
func injectedHelperBroadcastsFailClosedNativePollVoteMetadata() throws {
154154
let testFile = URL(fileURLWithPath: #filePath)
155155
let repoRoot =
156156
testFile
@@ -168,9 +168,17 @@ func injectedHelperWiresFailClosedNativePollVote() throws {
168168
#expect(sendVoteBody.contains("pollVoteMessageInitializerAvailable()"))
169169
#expect(!sendVoteBody.contains("pollPayloadMessageInitializerAvailable()"))
170170
#expect(voteBody.contains("associatedMessageType"))
171-
#expect(voteBody.contains("setBalloonBundleID:"))
172-
#expect(voteBody.contains("setPayloadData:"))
173-
#expect(voteBody.contains("return nil;"))
171+
#expect(voteBody.contains("BOOL balloonStamped = NO;"))
172+
#expect(voteBody.contains("BOOL payloadStamped = NO;"))
173+
#expect(
174+
voteBody.contains("if ([target respondsToSelector:@selector(setBalloonBundleID:)])")
175+
)
176+
#expect(voteBody.contains("if ([target respondsToSelector:@selector(setPayloadData:)])"))
177+
#expect(voteBody.contains("return (balloonStamped && payloadStamped) ? result : nil;"))
178+
#expect(
179+
!voteBody.contains(
180+
"|| ![target respondsToSelector:@selector(setPayloadData:)]"
181+
))
174182
}
175183

176184
@Test

docs/history.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ poll's stable option identifier before sending:
9292
imsg poll vote --chat-id <id> --poll <poll-guid> --option-index 2
9393
```
9494

95+
On macOS 26.4, use imsg 0.12.2 or later. Earlier builds could create a local
96+
vote row without the Polls payload, so the recipient's poll did not update.
97+
9598
### Manual native poll test plan
9699

97100
1. Create a native poll in Messages from an iPhone or Mac.

0 commit comments

Comments
 (0)