Skip to content

Commit d89e2e7

Browse files
authored
add nudge (#76)
* add nudge * Update NudgeButton.swift * server: fix update dialog types * apple: remove notification popover fixed sizing * apple: tighten notifications popover layout * apple: align notifications popover controls * apple: use capsule buttons in popovers * multi: add nudge tests - server encode nudge behavior - web protocol round-trip - InlineUI nudge helper coverage * multi: nudge media type
1 parent de48272 commit d89e2e7

30 files changed

Lines changed: 1139 additions & 80 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Nudge media type plan (2026-01-21)
2+
3+
## Goal
4+
Move nudge detection to `mediaType = "nudge"` (no emoji-based detection), add InputMedia support, and keep DB changes minimal.
5+
6+
## Steps
7+
- [x] Review current nudge handling in server encode/send paths, proto, and Apple send flows.
8+
- [x] Update proto InputMedia to include nudge and regenerate protocol outputs.
9+
- [x] Update server schema + send/encode logic to set/use `mediaType = "nudge"` and drop emoji detection (incl. notifications).
10+
- [x] Update Apple send path (NudgeButton + SendMessageTransaction helper) to send InputMedia.nudge while keeping text.
11+
- [x] Update/add tests to ensure only `mediaType = "nudge"` triggers nudge handling.
12+
13+
## Notes
14+
- Keep message text as "👋" for UI display, but rely on mediaType for detection.
15+
- Migration should only expand the existing mediaType enum/check.

apple/InlineIOS/Features/Chat/ChatView.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,30 @@ struct ChatView: View {
6868
.toolbar(.hidden, for: .tabBar)
6969
.toolbarRole(.editor)
7070
.toolbar {
71-
ToolbarItem(placement: .topBarTrailing) {
72-
TranslationButton(peer: peerId)
73-
.tint(ThemeManager.shared.accentColor)
71+
if peerId.isPrivate {
72+
if #available(iOS 26.0, *) {
73+
ToolbarItem(placement: .primaryAction) {
74+
NudgeButton(peer: peerId, chatId: fullChatViewModel.chat?.id)
75+
}
76+
ToolbarSpacer(.fixed, placement: .primaryAction)
77+
ToolbarItem(placement: .primaryAction) {
78+
TranslationButton(peer: peerId)
79+
.tint(ThemeManager.shared.accentColor)
80+
}
81+
} else {
82+
ToolbarItem(placement: .topBarTrailing) {
83+
NudgeButton(peer: peerId, chatId: fullChatViewModel.chat?.id)
84+
}
85+
ToolbarItem(placement: .primaryAction) {
86+
TranslationButton(peer: peerId)
87+
.tint(ThemeManager.shared.accentColor)
88+
}
89+
}
90+
} else {
91+
ToolbarItem(placement: .primaryAction) {
92+
TranslationButton(peer: peerId)
93+
.tint(ThemeManager.shared.accentColor)
94+
}
7495
}
7596

7697
if #available(iOS 26.0, *) {

apple/InlineIOS/Localizable.xcstrings

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,9 @@
392392
}
393393
}
394394
}
395+
},
396+
"Control DM notification behavior" : {
397+
395398
},
396399
"Control how you receive notifications" : {
397400

@@ -477,9 +480,15 @@
477480
}
478481
}
479482
}
483+
},
484+
"Direct Messages" : {
485+
480486
},
481487
"Direct updates applied" : {
482488

489+
},
490+
"Disable DM notifications" : {
491+
483492
},
484493
"Disconnect" : {
485494

@@ -726,6 +735,9 @@
726735
},
727736
"Mark Unread" : {
728737

738+
},
739+
"Mentions and nudges will still notify you" : {
740+
729741
},
730742
"name@example.com" : {
731743
"comment" : "Email placeholder",

apple/InlineIOS/UI/NotificationSettingsPopover.swift

Lines changed: 93 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ struct NotificationSettingsButton: View {
66
@EnvironmentObject private var notificationSettings: NotificationSettingsManager
77

88
@State private var presented = false
9+
@State private var showMentionsSettings = false
910

1011
var body: some View {
1112
button
@@ -22,8 +23,18 @@ struct NotificationSettingsButton: View {
2223
}
2324
}
2425
}
26+
.background(
27+
NavigationLink(
28+
destination: MentionsNotificationSettingsView(
29+
disableDmNotifications: $notificationSettings.disableDmNotifications
30+
),
31+
isActive: $showMentionsSettings
32+
) {
33+
EmptyView()
34+
}
35+
.hidden()
36+
)
2537

26-
.presentationDetents([.medium])
2738
.presentationDragIndicator(.visible)
2839
}
2940
}
@@ -86,7 +97,7 @@ struct NotificationSettingsButton: View {
8697
onChange: {
8798
notificationSettings.mode = $0
8899
close()
89-
},
100+
}
90101
)
91102

92103
NotificationSettingsItem(
@@ -136,48 +147,96 @@ private struct NotificationSettingsItem<Value: Equatable>: View {
136147
var selected: Bool
137148
var value: Value
138149
var onChange: (Value) -> Void
150+
var customizeAction: (() -> Void)?
139151
let theme = ThemeManager.shared.selected
140152

141153
var body: some View {
142-
Button {
154+
HStack(spacing: 12) {
155+
Circle()
156+
.fill(selected ? Color(theme.accent) : Color(.systemGray5))
157+
.frame(width: 36, height: 36)
158+
.overlay {
159+
Image(systemName: systemImage)
160+
.font(.system(size: 18, weight: .medium))
161+
.foregroundStyle(selected ? Color.white : Color(.systemGray))
162+
}
163+
164+
VStack(alignment: .leading, spacing: 2) {
165+
Text(title)
166+
.font(.body)
167+
.fontWeight(.medium)
168+
169+
Text(description)
170+
.font(.caption)
171+
172+
}
173+
174+
Spacer()
175+
176+
if let customizeAction {
177+
Button(action: customizeAction) {
178+
Circle()
179+
.frame(width: 28, height: 28)
180+
.foregroundStyle(Color(.systemGray5))
181+
.overlay {
182+
Image(systemName: "ellipsis")
183+
.font(.system(size: 13, weight: .medium))
184+
.foregroundStyle(.secondary)
185+
}
186+
.contentShape(Circle())
187+
}
188+
.buttonStyle(.plain)
189+
}
190+
191+
if selected {
192+
Image(systemName: "checkmark")
193+
.font(.system(size: 16, weight: .semibold))
194+
.foregroundStyle(Color(theme.accent))
195+
}
196+
}
197+
.padding(.vertical, 8)
198+
.padding(.horizontal, 12)
199+
.background(
200+
RoundedRectangle(cornerRadius: 12)
201+
.fill(selected ? Color(theme.accent).opacity(0.1) : ThemeManager.shared.cardBackgroundColor)
202+
)
203+
.contentShape(Rectangle())
204+
.onTapGesture {
143205
onChange(value)
144-
} label: {
145-
HStack(spacing: 12) {
146-
Circle()
147-
.fill(selected ? Color(theme.accent) : Color(.systemGray5))
148-
.frame(width: 36, height: 36)
149-
.overlay {
150-
Image(systemName: systemImage)
151-
.font(.system(size: 18, weight: .medium))
152-
.foregroundStyle(selected ? Color.white : Color(.systemGray))
153-
}
206+
}
207+
.animation(.easeOut(duration: 0.08), value: selected)
208+
}
209+
}
210+
211+
private struct MentionsNotificationSettingsView: View {
212+
@Binding var disableDmNotifications: Bool
213+
214+
var body: some View {
215+
VStack(alignment: .leading, spacing: 16) {
216+
VStack(alignment: .leading, spacing: 6) {
217+
Text("Direct Messages")
218+
.font(.headline)
154219

220+
Text("Control DM notification behavior")
221+
.font(.subheadline)
222+
.foregroundStyle(.secondary)
223+
}
224+
225+
Toggle(isOn: $disableDmNotifications) {
155226
VStack(alignment: .leading, spacing: 2) {
156-
Text(title)
227+
Text("Disable DM notifications")
157228
.font(.body)
158-
.fontWeight(.medium)
159-
160-
Text(description)
229+
Text("Mentions and nudges will still notify you")
161230
.font(.caption)
162-
163-
}
164-
165-
Spacer()
166-
167-
if selected {
168-
Image(systemName: "checkmark")
169-
.font(.system(size: 16, weight: .semibold))
170-
.foregroundStyle(Color(theme.accent))
231+
.foregroundStyle(.secondary)
171232
}
172233
}
173-
.padding(.vertical, 8)
174-
.padding(.horizontal, 12)
175-
.background(
176-
RoundedRectangle(cornerRadius: 12)
177-
.fill(selected ? Color(theme.accent).opacity(0.1) : ThemeManager.shared.cardBackgroundColor)
178-
)
234+
.toggleStyle(.switch)
235+
236+
Spacer()
179237
}
180-
.buttonStyle(.plain)
181-
.animation(.easeOut(duration: 0.08), value: selected)
238+
.padding(16)
239+
.navigationTitle("Direct Messages")
240+
.navigationBarTitleDisplayMode(.inline)
182241
}
183242
}

apple/InlineKit/Sources/InlineKit/Models/Message.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,8 @@ public extension InlineProtocol.Message {
502502
message
503503
} else if isSticker == true {
504504
"🖼️ Sticker"
505+
} else if case .nudge = media.media {
506+
"👋 Nudge"
505507
} else if media.photo.hasPhoto {
506508
"🖼️ Photo"
507509
} else if media.video.hasVideo {

apple/InlineKit/Sources/InlineKit/ProtocolHelpers/ProtocolMedia.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,10 @@ public extension InputMedia {
5151
})
5252
}
5353
}
54+
55+
static func fromNudge() -> InputMedia {
56+
InputMedia.with {
57+
$0.media = .nudge(.init())
58+
}
59+
}
5460
}

apple/InlineKit/Sources/InlineKit/Transactions2/SendMessageTransaction.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public struct SendMessageTransaction: Transaction2 {
2424
public var chatId: Int64
2525
public var replyToMsgId: Int64?
2626
public var isSticker: Bool?
27+
public var isNudge: Bool
2728
public var entities: MessageEntities?
2829
public var sendMode: MessageSendMode?
2930
public var randomId: Int64
@@ -35,6 +36,7 @@ public struct SendMessageTransaction: Transaction2 {
3536
case chatId
3637
case replyToMsgId
3738
case isSticker
39+
case isNudge
3840
case entities
3941
case sendMode
4042
case randomId
@@ -47,6 +49,7 @@ public struct SendMessageTransaction: Transaction2 {
4749
chatId: Int64,
4850
replyToMsgId: Int64?,
4951
isSticker: Bool?,
52+
isNudge: Bool,
5053
entities: MessageEntities?,
5154
sendMode: MessageSendMode?,
5255
randomId: Int64,
@@ -57,6 +60,7 @@ public struct SendMessageTransaction: Transaction2 {
5760
self.chatId = chatId
5861
self.replyToMsgId = replyToMsgId
5962
self.isSticker = isSticker
63+
self.isNudge = isNudge
6064
self.entities = entities
6165
self.sendMode = sendMode
6266
self.randomId = randomId
@@ -70,6 +74,7 @@ public struct SendMessageTransaction: Transaction2 {
7074
chatId = try container.decode(Int64.self, forKey: .chatId)
7175
replyToMsgId = try container.decodeIfPresent(Int64.self, forKey: .replyToMsgId)
7276
isSticker = try container.decodeIfPresent(Bool.self, forKey: .isSticker)
77+
isNudge = try container.decodeIfPresent(Bool.self, forKey: .isNudge) ?? false
7378
entities = try container.decodeIfPresent(MessageEntities.self, forKey: .entities)
7479
if let rawValue = try container.decodeIfPresent(Int.self, forKey: .sendMode) {
7580
sendMode = MessageSendMode(rawValue: rawValue) ?? .modeUnspecified
@@ -87,6 +92,9 @@ public struct SendMessageTransaction: Transaction2 {
8792
try container.encode(chatId, forKey: .chatId)
8893
try container.encodeIfPresent(replyToMsgId, forKey: .replyToMsgId)
8994
try container.encodeIfPresent(isSticker, forKey: .isSticker)
95+
if isNudge {
96+
try container.encode(isNudge, forKey: .isNudge)
97+
}
9098
try container.encodeIfPresent(entities, forKey: .entities)
9199
if let sendMode = sendMode {
92100
try container.encode(sendMode.rawValue, forKey: .sendMode)
@@ -102,6 +110,7 @@ public struct SendMessageTransaction: Transaction2 {
102110
chatId: Int64,
103111
replyToMsgId: Int64? = nil,
104112
isSticker: Bool? = nil,
113+
isNudge: Bool = false,
105114
entities: MessageEntities? = nil,
106115
sendMode: MessageSendMode? = nil
107116
) {
@@ -112,6 +121,7 @@ public struct SendMessageTransaction: Transaction2 {
112121
chatId: chatId,
113122
replyToMsgId: replyToMsgId,
114123
isSticker: isSticker,
124+
isNudge: isNudge,
115125
entities: entities,
116126
sendMode: sendMode,
117127
randomId: randomId,
@@ -130,6 +140,7 @@ public struct SendMessageTransaction: Transaction2 {
130140
if let replyToMsgId = context.replyToMsgId { $0.replyToMsgID = replyToMsgId }
131141
if let entities = context.entities { $0.entities = entities }
132142
if let sendMode = context.sendMode { $0.sendMode = sendMode }
143+
if context.isNudge { $0.media = InputMedia.fromNudge() }
133144
})
134145
}
135146

@@ -294,6 +305,7 @@ public extension Transaction2 where Self == SendMessageTransaction {
294305
chatId: Int64,
295306
replyToMsgId: Int64? = nil,
296307
isSticker: Bool? = nil,
308+
isNudge: Bool = false,
297309
entities: MessageEntities? = nil,
298310
sendMode: MessageSendMode? = nil
299311
) -> SendMessageTransaction {
@@ -303,6 +315,7 @@ public extension Transaction2 where Self == SendMessageTransaction {
303315
chatId: chatId,
304316
replyToMsgId: replyToMsgId,
305317
isSticker: isSticker,
318+
isNudge: isNudge,
306319
entities: entities,
307320
sendMode: sendMode
308321
)

apple/InlineKit/Sources/InlineKit/UserSettings/INUserSettings.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public class INUserSettings {
6565
// Update current settings with cached values
6666
notification.mode = cachedSettings.mode
6767
notification.silent = cachedSettings.silent
68+
notification.disableDmNotifications = cachedSettings.disableDmNotifications
6869
notification.requiresMention = cachedSettings.requiresMention
6970
notification.usesDefaultRules = cachedSettings.usesDefaultRules
7071
notification.customRules = cachedSettings.customRules

0 commit comments

Comments
 (0)