Skip to content
Draft
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 PocketMesh/Extensions/View+ListChevron.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import SwiftUI

extension View {
/// Adds a trailing chevron aligned with the top row of a list item.
/// Used with hidden NavigationLink pattern to control chevron positioning.
func listChevron(offset: CGFloat = -8) -> some View {
HStack(spacing: 12) {
self
Image(systemName: "chevron.right")
.font(.caption.weight(.semibold))
.foregroundStyle(.tertiary)
.offset(y: offset)
}
}
}
19 changes: 8 additions & 11 deletions PocketMesh/Views/Chats/ChannelConversationRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,27 @@ struct ChannelConversationRow: View {
var body: some View {
HStack(spacing: 12) {
ChannelAvatar(channel: channel, size: 44)
.overlay(alignment: .topTrailing) {
UnreadCountBadge(count: channel.unreadCount)
}

VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Text(channel.name.isEmpty ? L10n.Chats.Chats.Channel.defaultName(Int(channel.index)) : channel.name)
.font(.headline)
.lineLimit(1)

Spacer()

NotificationLevelIndicator(level: channel.notificationLevel)

if channel.isFavorite {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.font(.caption)
.font(.system(size: 13.2))
.accessibilityLabel(Strings.favorite)
}

Spacer()

NotificationLevelIndicator(level: channel.notificationLevel)

if let date = channel.lastMessageDate {
ConversationTimestamp(date: date)
}
Expand All @@ -39,12 +42,6 @@ struct ChannelConversationRow: View {
.lineLimit(1)

Spacer()

UnreadBadges(
unreadCount: channel.unreadCount,
unreadMentionCount: channel.unreadMentionCount,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed? This will remove the "@" badge that appears when a user was mentioned in a chat/channel.

notificationLevel: channel.notificationLevel
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this will remove the ability to color the unread badge based on the set notification level

)
}
}
.alignmentGuide(.listRowSeparatorLeading) { d in
Expand Down
30 changes: 18 additions & 12 deletions PocketMesh/Views/Chats/ConversationListContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,26 @@ struct ConversationListContent: View {
let route = ChatRoute(conversation: conversation)
switch conversation {
case .direct(let contact):
NavigationLink(value: route) {
ConversationRow(contact: contact, viewModel: viewModel)
}
.conversationSwipeActions(conversation: conversation, viewModel: viewModel) {
onDeleteConversation(conversation)
}
ConversationRow(contact: contact, viewModel: viewModel)
.listChevron(offset: -11)
.background {
NavigationLink(value: route) { EmptyView() }
.opacity(0)
}
.conversationSwipeActions(conversation: conversation, viewModel: viewModel) {
onDeleteConversation(conversation)
}

case .channel(let channel):
NavigationLink(value: route) {
ChannelConversationRow(channel: channel, viewModel: viewModel)
}
.conversationSwipeActions(conversation: conversation, viewModel: viewModel) {
onDeleteConversation(conversation)
}
ChannelConversationRow(channel: channel, viewModel: viewModel)
.listChevron(offset: -11)
.background {
NavigationLink(value: route) { EmptyView() }
.opacity(0)
}
.conversationSwipeActions(conversation: conversation, viewModel: viewModel) {
onDeleteConversation(conversation)
}

case .room(let session):
Button {
Expand Down
19 changes: 8 additions & 11 deletions PocketMesh/Views/Chats/ConversationRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,30 @@ struct ConversationRow: View {
var body: some View {
HStack(spacing: 12) {
ContactAvatar(contact: contact, size: 44)
.overlay(alignment: .topTrailing) {
UnreadCountBadge(count: contact.unreadCount)
}

VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Text(contact.displayName)
.font(.headline)
.lineLimit(1)

Spacer()

MutedIndicator(isMuted: contact.isMuted)

if viewModel.togglingFavoriteID == contact.id {
ProgressView()
.controlSize(.small)
} else if contact.isFavorite {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.font(.caption)
.font(.system(size: 13.2))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use specific font sizes, stick with dynamic. To make it a little larger than .caption, try .footnote.

.accessibilityLabel(L10n.Chats.Chats.Row.favorite)
}

Spacer()

MutedIndicator(isMuted: contact.isMuted)

if let date = contact.lastMessageDate {
ConversationTimestamp(date: date)
}
Expand All @@ -41,12 +44,6 @@ struct ConversationRow: View {
.lineLimit(1)

Spacer()

UnreadBadges(
unreadCount: contact.unreadCount,
unreadMentionCount: contact.unreadMentionCount,
notificationLevel: contact.isMuted ? .muted : .all
)
}
}
.alignmentGuide(.listRowSeparatorLeading) { d in
Expand Down
8 changes: 6 additions & 2 deletions PocketMesh/Views/Chats/ConversationTimestamp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ struct ConversationTimestamp: View {
if calendar.isDateInToday(date) {
return date.formatted(date: .omitted, time: .shortened)
} else if calendar.isDateInYesterday(date) {
return date.formatted(.relative(presentation: .named))
} else {
return "Yesterday"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't hard code strings, use date.formatted(.relative(presentation: .named)) like it was before. It will handle translating "yesterday" for free.

} else if let daysAgo = calendar.dateComponents([.day], from: date, to: now).day, daysAgo < 7 {
return date.formatted(.dateTime.weekday(.wide))
} else if calendar.component(.year, from: date) == calendar.component(.year, from: now) {
return date.formatted(.dateTime.month(.abbreviated).day())
} else {
return date.formatted(.dateTime.month(.abbreviated).day().year())
}
}
}
21 changes: 10 additions & 11 deletions PocketMesh/Views/Chats/RoomConversationRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,29 @@ struct RoomConversationRow: View {
let session: RemoteNodeSessionDTO

var body: some View {
HStack(spacing: 12) {
HStack(alignment: .center, spacing: 12) {
NodeAvatar(publicKey: session.publicKey, role: .roomServer, size: 44)
.overlay(alignment: .topTrailing) {
UnreadCountBadge(count: session.unreadCount)
}

VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Text(session.name)
.font(.headline)
.lineLimit(1)

Spacer()

NotificationLevelIndicator(level: session.notificationLevel)

if session.isFavorite {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.font(.caption)
.font(.system(size: 13.2))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use dynamic type

.accessibilityLabel(L10n.Chats.Chats.Row.favorite)
}

Spacer()

NotificationLevelIndicator(level: session.notificationLevel)

if let date = session.lastMessageDate {
ConversationTimestamp(date: date)
}
Expand All @@ -42,11 +45,6 @@ struct RoomConversationRow: View {
}

Spacer()

UnreadBadges(
unreadCount: session.unreadCount,
notificationLevel: session.notificationLevel
)
}
}
.alignmentGuide(.listRowSeparatorLeading) { d in
Expand All @@ -56,6 +54,7 @@ struct RoomConversationRow: View {
Image(systemName: "chevron.right")
.font(.caption.weight(.semibold))
.foregroundStyle(.tertiary)
.offset(y: -11)
}
.padding(.vertical, 4)
.contentShape(.rect)
Expand Down
31 changes: 17 additions & 14 deletions PocketMesh/Views/Components/RelativeTimestampText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@ import SwiftUI
struct RelativeTimestampText: View {
let timestamp: UInt32

private static let relativeFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .abbreviated
return formatter
}()

private static let weekThreshold: TimeInterval = 604_800
private static let nowThreshold: TimeInterval = 60

var body: some View {
TimelineView(.everyMinute) { context in
Text(Self.format(timestamp: timestamp, relativeTo: context.date))
Expand All @@ -24,17 +15,29 @@ struct RelativeTimestampText: View {
/// Formats a timestamp relative to the given date. Exposed for testing.
static func format(timestamp: UInt32, relativeTo now: Date) -> String {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp))
let interval = now.timeIntervalSince(date)
let calendar = Calendar.current

let startOfNow = calendar.startOfDay(for: now)
let startOfDate = calendar.startOfDay(for: date)
let daysAgo = calendar.dateComponents([.day], from: startOfDate, to: startOfNow).day ?? 0

if daysAgo == 0 {
return date.formatted(date: .omitted, time: .shortened)
}

if daysAgo == 1 {
return "Yesterday"
}

if interval < nowThreshold {
return L10n.Chats.Chats.Timestamp.now
if daysAgo < 7 {
return date.formatted(.dateTime.weekday(.wide))
}

if interval >= weekThreshold {
if calendar.component(.year, from: date) == calendar.component(.year, from: now) {
return date.formatted(.dateTime.month(.abbreviated).day())
}

return relativeFormatter.localizedString(for: date, relativeTo: now)
return date.formatted(.dateTime.month(.abbreviated).day().year())
}
}

Expand Down
45 changes: 45 additions & 0 deletions PocketMesh/Views/Components/UnreadCountBadge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import SwiftUI

/// Red badge with unread count, displayed as an overlay on avatars.
/// Matches iOS app icon badge style.
struct UnreadCountBadge: View {
let count: Int

var body: some View {
if count > 0 {
Text(count, format: .number)
.font(.caption2.bold())
.foregroundStyle(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(.red, in: .capsule)
.offset(x: 4, y: -4)
}
}
}

#Preview {
VStack(spacing: 20) {
Circle()
.fill(.blue)
.frame(width: 44, height: 44)
.overlay(alignment: .topTrailing) {
UnreadCountBadge(count: 3)
}

Circle()
.fill(.blue)
.frame(width: 44, height: 44)
.overlay(alignment: .topTrailing) {
UnreadCountBadge(count: 42)
}

Circle()
.fill(.blue)
.frame(width: 44, height: 44)
.overlay(alignment: .topTrailing) {
UnreadCountBadge(count: 0)
}
}
.padding()
}
6 changes: 3 additions & 3 deletions PocketMesh/Views/Contacts/ContactRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@ struct ContactRowView: View {
.accessibilityLabel(L10n.Contacts.Contacts.Row.blocked)
}

Spacer()

if isTogglingFavorite {
ProgressView()
.controlSize(.small)
} else if contact.isFavorite {
Image(systemName: "star.fill")
.font(.caption)
.font(.system(size: 13.2))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, use dynamic type.

.foregroundStyle(.yellow)
.accessibilityLabel(L10n.Contacts.Contacts.Row.favorite)
}

Spacer()

RelativeTimestampText(timestamp: contact.lastAdvertTimestamp)
}

Expand Down
19 changes: 11 additions & 8 deletions PocketMesh/Views/Contacts/ContactsCompactList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ struct ContactsCompactList: View {
.listSectionSeparator(.hidden)

ForEach(Array(filteredContacts.enumerated()), id: \.element.id) { index, contact in
NavigationLink(value: contact) {
ContactRowView(
contact: contact,
showTypeLabel: isSearching,
userLocation: appState.locationService.currentLocation,
index: index,
isTogglingFavorite: viewModel.togglingFavoriteID == contact.id
)
ContactRowView(
contact: contact,
showTypeLabel: isSearching,
userLocation: appState.locationService.currentLocation,
index: index,
isTogglingFavorite: viewModel.togglingFavoriteID == contact.id
)
.listChevron()
.background {
NavigationLink(value: contact) { EmptyView() }
.opacity(0)
}
.contactSwipeActions(contact: contact, viewModel: viewModel)
}
Expand Down
Loading