Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .agent-docs/2026-01-21-nudge-media-type-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Nudge media type plan (2026-01-21)

## Goal
Move nudge detection to `mediaType = "nudge"` (no emoji-based detection), add InputMedia support, and keep DB changes minimal.

## Steps
- [x] Review current nudge handling in server encode/send paths, proto, and Apple send flows.
- [x] Update proto InputMedia to include nudge and regenerate protocol outputs.
- [x] Update server schema + send/encode logic to set/use `mediaType = "nudge"` and drop emoji detection (incl. notifications).
- [x] Update Apple send path (NudgeButton + SendMessageTransaction helper) to send InputMedia.nudge while keeping text.
- [x] Update/add tests to ensure only `mediaType = "nudge"` triggers nudge handling.

## Notes
- Keep message text as "👋" for UI display, but rely on mediaType for detection.
- Migration should only expand the existing mediaType enum/check.
27 changes: 24 additions & 3 deletions apple/InlineIOS/Features/Chat/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,30 @@ struct ChatView: View {
.toolbar(.hidden, for: .tabBar)
.toolbarRole(.editor)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
TranslationButton(peer: peerId)
.tint(ThemeManager.shared.accentColor)
if peerId.isPrivate {
if #available(iOS 26.0, *) {
ToolbarItem(placement: .primaryAction) {
NudgeButton(peer: peerId, chatId: fullChatViewModel.chat?.id)
}
ToolbarSpacer(.fixed, placement: .primaryAction)
ToolbarItem(placement: .primaryAction) {
TranslationButton(peer: peerId)
.tint(ThemeManager.shared.accentColor)
}
} else {
ToolbarItem(placement: .topBarTrailing) {
NudgeButton(peer: peerId, chatId: fullChatViewModel.chat?.id)
}
ToolbarItem(placement: .primaryAction) {
TranslationButton(peer: peerId)
.tint(ThemeManager.shared.accentColor)
}
}
} else {
ToolbarItem(placement: .primaryAction) {
TranslationButton(peer: peerId)
.tint(ThemeManager.shared.accentColor)
}
}

if #available(iOS 26.0, *) {
Expand Down
12 changes: 12 additions & 0 deletions apple/InlineIOS/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,9 @@
}
}
}
},
"Control DM notification behavior" : {

},
"Control how you receive notifications" : {

Expand Down Expand Up @@ -477,9 +480,15 @@
}
}
}
},
"Direct Messages" : {

},
"Direct updates applied" : {

},
"Disable DM notifications" : {

},
"Disconnect" : {

Expand Down Expand Up @@ -726,6 +735,9 @@
},
"Mark Unread" : {

},
"Mentions and nudges will still notify you" : {

},
"name@example.com" : {
"comment" : "Email placeholder",
Expand Down
127 changes: 93 additions & 34 deletions apple/InlineIOS/UI/NotificationSettingsPopover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct NotificationSettingsButton: View {
@EnvironmentObject private var notificationSettings: NotificationSettingsManager

@State private var presented = false
@State private var showMentionsSettings = false

var body: some View {
button
Expand All @@ -22,8 +23,18 @@ struct NotificationSettingsButton: View {
}
}
}
.background(
NavigationLink(
destination: MentionsNotificationSettingsView(
disableDmNotifications: $notificationSettings.disableDmNotifications
),
isActive: $showMentionsSettings
) {
EmptyView()
}
.hidden()
)

.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
Expand Down Expand Up @@ -86,7 +97,7 @@ struct NotificationSettingsButton: View {
onChange: {
notificationSettings.mode = $0
close()
},
}
)

NotificationSettingsItem(
Expand Down Expand Up @@ -136,48 +147,96 @@ private struct NotificationSettingsItem<Value: Equatable>: View {
var selected: Bool
var value: Value
var onChange: (Value) -> Void
var customizeAction: (() -> Void)?
let theme = ThemeManager.shared.selected

var body: some View {
Button {
HStack(spacing: 12) {
Circle()
.fill(selected ? Color(theme.accent) : Color(.systemGray5))
.frame(width: 36, height: 36)
.overlay {
Image(systemName: systemImage)
.font(.system(size: 18, weight: .medium))
.foregroundStyle(selected ? Color.white : Color(.systemGray))
}

VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.body)
.fontWeight(.medium)

Text(description)
.font(.caption)

}

Spacer()

if let customizeAction {
Button(action: customizeAction) {
Circle()
.frame(width: 28, height: 28)
.foregroundStyle(Color(.systemGray5))
.overlay {
Image(systemName: "ellipsis")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.secondary)
}
.contentShape(Circle())
}
.buttonStyle(.plain)
}

if selected {
Image(systemName: "checkmark")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Color(theme.accent))
}
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(selected ? Color(theme.accent).opacity(0.1) : ThemeManager.shared.cardBackgroundColor)
)
.contentShape(Rectangle())
.onTapGesture {
onChange(value)
} label: {
HStack(spacing: 12) {
Circle()
.fill(selected ? Color(theme.accent) : Color(.systemGray5))
.frame(width: 36, height: 36)
.overlay {
Image(systemName: systemImage)
.font(.system(size: 18, weight: .medium))
.foregroundStyle(selected ? Color.white : Color(.systemGray))
}
}
.animation(.easeOut(duration: 0.08), value: selected)
}
}

private struct MentionsNotificationSettingsView: View {
@Binding var disableDmNotifications: Bool

var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 6) {
Text("Direct Messages")
.font(.headline)

Text("Control DM notification behavior")
.font(.subheadline)
.foregroundStyle(.secondary)
}

Toggle(isOn: $disableDmNotifications) {
VStack(alignment: .leading, spacing: 2) {
Text(title)
Text("Disable DM notifications")
.font(.body)
.fontWeight(.medium)

Text(description)
Text("Mentions and nudges will still notify you")
.font(.caption)

}

Spacer()

if selected {
Image(systemName: "checkmark")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Color(theme.accent))
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(selected ? Color(theme.accent).opacity(0.1) : ThemeManager.shared.cardBackgroundColor)
)
.toggleStyle(.switch)

Spacer()
}
.buttonStyle(.plain)
.animation(.easeOut(duration: 0.08), value: selected)
.padding(16)
.navigationTitle("Direct Messages")
.navigationBarTitleDisplayMode(.inline)
}
}
2 changes: 2 additions & 0 deletions apple/InlineKit/Sources/InlineKit/Models/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,8 @@ public extension InlineProtocol.Message {
message
} else if isSticker == true {
"🖼️ Sticker"
} else if case .nudge = media.media {
"👋 Nudge"
} else if media.photo.hasPhoto {
"🖼️ Photo"
} else if media.video.hasVideo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,10 @@ public extension InputMedia {
})
}
}

static func fromNudge() -> InputMedia {
InputMedia.with {
$0.media = .nudge(.init())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public struct SendMessageTransaction: Transaction2 {
public var chatId: Int64
public var replyToMsgId: Int64?
public var isSticker: Bool?
public var isNudge: Bool
public var entities: MessageEntities?
public var sendMode: MessageSendMode?
public var randomId: Int64
Expand All @@ -35,6 +36,7 @@ public struct SendMessageTransaction: Transaction2 {
case chatId
case replyToMsgId
case isSticker
case isNudge
case entities
case sendMode
case randomId
Expand All @@ -47,6 +49,7 @@ public struct SendMessageTransaction: Transaction2 {
chatId: Int64,
replyToMsgId: Int64?,
isSticker: Bool?,
isNudge: Bool,
entities: MessageEntities?,
sendMode: MessageSendMode?,
randomId: Int64,
Expand All @@ -57,6 +60,7 @@ public struct SendMessageTransaction: Transaction2 {
self.chatId = chatId
self.replyToMsgId = replyToMsgId
self.isSticker = isSticker
self.isNudge = isNudge
self.entities = entities
self.sendMode = sendMode
self.randomId = randomId
Expand All @@ -70,6 +74,7 @@ public struct SendMessageTransaction: Transaction2 {
chatId = try container.decode(Int64.self, forKey: .chatId)
replyToMsgId = try container.decodeIfPresent(Int64.self, forKey: .replyToMsgId)
isSticker = try container.decodeIfPresent(Bool.self, forKey: .isSticker)
isNudge = try container.decodeIfPresent(Bool.self, forKey: .isNudge) ?? false
entities = try container.decodeIfPresent(MessageEntities.self, forKey: .entities)
if let rawValue = try container.decodeIfPresent(Int.self, forKey: .sendMode) {
sendMode = MessageSendMode(rawValue: rawValue) ?? .modeUnspecified
Expand All @@ -87,6 +92,9 @@ public struct SendMessageTransaction: Transaction2 {
try container.encode(chatId, forKey: .chatId)
try container.encodeIfPresent(replyToMsgId, forKey: .replyToMsgId)
try container.encodeIfPresent(isSticker, forKey: .isSticker)
if isNudge {
try container.encode(isNudge, forKey: .isNudge)
}
try container.encodeIfPresent(entities, forKey: .entities)
if let sendMode = sendMode {
try container.encode(sendMode.rawValue, forKey: .sendMode)
Expand All @@ -102,6 +110,7 @@ public struct SendMessageTransaction: Transaction2 {
chatId: Int64,
replyToMsgId: Int64? = nil,
isSticker: Bool? = nil,
isNudge: Bool = false,
entities: MessageEntities? = nil,
sendMode: MessageSendMode? = nil
) {
Expand All @@ -112,6 +121,7 @@ public struct SendMessageTransaction: Transaction2 {
chatId: chatId,
replyToMsgId: replyToMsgId,
isSticker: isSticker,
isNudge: isNudge,
entities: entities,
sendMode: sendMode,
randomId: randomId,
Expand All @@ -130,6 +140,7 @@ public struct SendMessageTransaction: Transaction2 {
if let replyToMsgId = context.replyToMsgId { $0.replyToMsgID = replyToMsgId }
if let entities = context.entities { $0.entities = entities }
if let sendMode = context.sendMode { $0.sendMode = sendMode }
if context.isNudge { $0.media = InputMedia.fromNudge() }
})
}

Expand Down Expand Up @@ -294,6 +305,7 @@ public extension Transaction2 where Self == SendMessageTransaction {
chatId: Int64,
replyToMsgId: Int64? = nil,
isSticker: Bool? = nil,
isNudge: Bool = false,
entities: MessageEntities? = nil,
sendMode: MessageSendMode? = nil
) -> SendMessageTransaction {
Expand All @@ -303,6 +315,7 @@ public extension Transaction2 where Self == SendMessageTransaction {
chatId: chatId,
replyToMsgId: replyToMsgId,
isSticker: isSticker,
isNudge: isNudge,
entities: entities,
sendMode: sendMode
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public class INUserSettings {
// Update current settings with cached values
notification.mode = cachedSettings.mode
notification.silent = cachedSettings.silent
notification.disableDmNotifications = cachedSettings.disableDmNotifications
notification.requiresMention = cachedSettings.requiresMention
notification.usesDefaultRules = cachedSettings.usesDefaultRules
notification.customRules = cachedSettings.customRules
Expand Down
Loading
Loading