Skip to content

Commit

Permalink
Extends example app to include edit and delete functionality in `.moc…
Browse files Browse the repository at this point in the history
…k` and `.live` mode
  • Loading branch information
umair-ably committed Feb 26, 2025
1 parent 80e0a40 commit 83ef89a
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 57 deletions.
207 changes: 155 additions & 52 deletions Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,29 @@ struct ContentView: View {
@State private var chatClient = Environment.current.createChatClient()

@State private var title = "Room"
@State private var messages = [BasicListItem]()
@State private var reactions: [Reaction] = []
@State private var newMessage = ""
@State private var typingInfo = ""
@State private var occupancyInfo = "Connections: 0"
@State private var statusInfo = ""

@State private var listItems = [ListItem]()
@State private var editingItemID: String?

enum ListItem: Identifiable {
case message(MessageListItem)
case presence(PresenceListItem)

var id: String {
switch self {
case let .message(item):
item.message.id
case let .presence(item):
item.presence.timestamp.description
}
}
}

private func room() async throws -> Room {
try await chatClient.rooms.get(
roomID: roomID,
Expand All @@ -61,7 +77,17 @@ struct ContentView: View {
}

private var sendTitle: String {
newMessage.isEmpty ? ReactionType.like.emoji : "Send"
if newMessage.isEmpty {
ReactionType.like.emoji
} else if editingItemID != nil {
"Update"
} else {
"Send"
}
}

func onMessageDelete(message: Message) async throws {
_ = try await room().messages.delete(message: message, params: .init())
}

var body: some View {
Expand All @@ -78,17 +104,51 @@ struct ContentView: View {
.font(.footnote)
.frame(height: 12)
.padding(.horizontal, 8)
List(messages, id: \.id) { item in
MessageBasicView(item: item)
.flip()
List(listItems, id: \.id) { item in
switch item {
case let .message(messageItem):
if messageItem.message.action == .delete {
DeletedMessageView(item: messageItem)
.flip()
} else {
MessageView(
item: messageItem,
isEditing: Binding(get: {
editingItemID == messageItem.message.id
}, set: { editing in
editingItemID = editing ? messageItem.message.id : nil
newMessage = editing ? messageItem.message.text : ""
})
) {
Task {
try await onMessageDelete(message: messageItem.message)
}
}.id(item.id)
.flip()
}
case let .presence(item):
PresenceMessageView(item: item)
.flip()
}
}
.flip()
.listStyle(PlainListStyle())
HStack {
TextField("Type a message...", text: $newMessage)
.onChange(of: newMessage) {
Task {
try await startTyping()
// this ensures that typing events are sent only when the message is actually changed whilst editing
if let index = listItems.firstIndex(where: { $0.id == editingItemID }) {
if case let .message(messageItem) = listItems[index] {
if newMessage != messageItem.message.text {
Task {
try await startTyping()
}
}
}
} else {
Task {
try await startTyping()
}
}
}
#if !os(tvOS)
Expand All @@ -106,7 +166,16 @@ struct ContentView: View {
Text(sendTitle)
#endif
}
if editingItemID != nil {
Button("", systemImage: "xmark.circle.fill") {
editingItemID = nil
newMessage = ""
}
.foregroundStyle(.red.opacity(0.8))
.transition(.scale.combined(with: .opacity))
}
}
.animation(.easeInOut, value: editingItemID)
.padding(.horizontal, 12)
HStack {
Text(typingInfo)
Expand Down Expand Up @@ -153,6 +222,11 @@ struct ContentView: View {
Task {
try await sendReaction(type: ReactionType.like.emoji)
}
} else if editingItemID != nil {
Task {
try await sendEditedMessage()
editingItemID = nil
}
} else {
Task {
try await sendMessage()
Expand All @@ -173,16 +247,39 @@ struct ContentView: View {
let previousMessages = try await messagesSubscription.getPreviousMessages(params: .init())

for message in previousMessages.items {
withAnimation {
messages.append(BasicListItem(id: message.serial, title: message.clientID, text: message.text))
switch message.action {
case .create, .update, .delete:
withAnimation {
listItems.append(.message(.init(message: message, isSender: message.clientID == chatClient.realtime.clientId)))
}
}
}

// Continue listening for messages on a background task so this function can return
Task {
for await message in messagesSubscription {
withAnimation {
messages.insert(BasicListItem(id: message.serial, title: message.clientID, text: message.text), at: 0)
switch message.action {
case .create:
withAnimation {
listItems.insert(
.message(
.init(
message: message,
isSender: message.clientID == chatClient.realtime.clientId
)
),
at: 0
)
}
case .update, .delete:
if let index = listItems.firstIndex(where: { $0.id == message.id }) {
listItems[index] = .message(
.init(
message: message,
isSender: message.clientID == chatClient.realtime.clientId
)
)
}
}
}
}
Expand All @@ -208,11 +305,14 @@ struct ContentView: View {
Task {
for await event in try await room().presence.subscribe(events: [.enter, .leave, .update]) {
withAnimation {
let status = event.data?.objectValue?["status"]?.stringValue
let clientPresenceChangeMessage = "\(event.clientID) \(event.action.displayedText)"
let presenceMessage = status != nil ? "\(clientPresenceChangeMessage) with status: \(status!)" : clientPresenceChangeMessage

messages.insert(BasicListItem(id: UUID().uuidString, title: "System", text: presenceMessage), at: 0)
listItems.insert(
.presence(
.init(
presence: event
)
),
at: 0
)
}
}
}
Expand Down Expand Up @@ -291,12 +391,45 @@ struct ContentView: View {
newMessage = ""
}

func sendEditedMessage() async throws {
guard !newMessage.isEmpty else {
return
}

if let editingMessageItem = listItems.compactMap({ listItem -> MessageListItem? in
if case let .message(message) = listItem, message.message.id == editingItemID {
return message
}
return nil
}).first {
let editedMessage = editingMessageItem.message.copy(text: newMessage)
_ = try await room().messages.update(newMessage: editedMessage, description: nil, metadata: nil)
}

newMessage = ""
}

func deleteMessage() async throws {
let messagesSubscription = try await room().messages.subscribe()
let previousMessages = try await messagesSubscription.getPreviousMessages(params: .init())

guard let message = previousMessages.items.first else {
return
}

_ = try await room().messages.delete(message: message, params: .init())
}

func sendReaction(type: String) async throws {
try await room().reactions.send(params: .init(type: type))
}

func startTyping() async throws {
try await room().typing.start()
if newMessage.isEmpty {
try await room().typing.stop()
} else {
try await room().typing.start()
}
}
}

Expand Down Expand Up @@ -360,41 +493,6 @@ extension ContentView {
}
}

struct BasicListItem {
var id: String
var title: String
var text: String
}

struct MessageBasicView: View {
var item: BasicListItem

var body: some View {
HStack {
VStack {
Text("\(item.title):")
.foregroundColor(.blue)
.bold()
Spacer()
}
VStack {
Text(item.text)
Spacer()
}
}
#if !os(tvOS)
.listRowSeparator(.hidden)
#endif
}
}

extension View {
func flip() -> some View {
rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
}

#Preview {
ContentView()
}
Expand Down Expand Up @@ -424,4 +522,9 @@ extension View {
}
}
}

func flip() -> some View {
rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
}
23 changes: 23 additions & 0 deletions Example/AblyChatExample/MessageViews/DeletedMessageView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import SwiftUI

struct DeletedMessageView: View {
var item: MessageListItem

var body: some View {
HStack(alignment: .firstTextBaseline) {
VStack {
Text("\(item.message.clientID):")
.foregroundColor(.blue)
.bold()
}
VStack {
Text("This message was deleted.")
.foregroundStyle(.secondary)
.italic()
}
}
#if !os(tvOS)
.listRowSeparator(.hidden)
#endif
}
}
20 changes: 20 additions & 0 deletions Example/AblyChatExample/MessageViews/MenuButtonView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import SwiftUI

struct MenuButtonView: View {
var onEdit: () -> Void
var onDelete: () -> Void

var body: some View {
Menu {
Button(action: onEdit) {
Label("Edit", systemImage: "pencil")
}

Button(role: .destructive, action: onDelete) {
Label("Delete", systemImage: "trash")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
Loading

0 comments on commit 83ef89a

Please sign in to comment.