Skip to content

Commit

Permalink
Initial implementation of the example app's functionality (ground wor…
Browse files Browse the repository at this point in the history
…k for #69).
  • Loading branch information
maratal committed Sep 27, 2024
1 parent dd103e6 commit 24cd34b
Showing 1 changed file with 313 additions and 8 deletions.
321 changes: 313 additions & 8 deletions Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,329 @@
import AblyChat
import SwiftUI

@MainActor
struct ContentView: View {
/// Just used to check that we can successfully import and use the AblyChat library. TODO remove this once we start building the library
@State private var ablyChatClient = DefaultChatClient(
#if os(macOS)
let screenWidth = NSScreen.main?.frame.width ?? 500
let screenHeight = NSScreen.main?.frame.height ?? 500
#else
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
#endif

@State private var chatClient = MockChatClient(
realtime: MockRealtime.create(),
clientOptions: ClientOptions()
)

@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 = ""

private func room() async throws -> Room {
try await chatClient.rooms.get(roomID: "Demo", options: .init())
}

private var sendTitle: String {
newMessage.isEmpty ? ReactionType.like.emoji : "Send"
}

var body: some View {
ZStack {
VStack {
Text(title)
.font(.headline)
.padding(5)
HStack {
Text("")
Text(occupancyInfo)
Text(statusInfo)
}
.font(.footnote)
.frame(height: 12)
.padding(.horizontal, 8)
List(messages, id: \.id) { item in
MessageBasicView(item: item)
.flip()
}
.flip()
.listStyle(PlainListStyle())
HStack {
TextField("Type a message...", text: $newMessage)
#if !os(tvOS)
.textFieldStyle(RoundedBorderTextFieldStyle())
#endif
Button(action: sendButtonAction) {
#if os(iOS)
Text(sendTitle)
.foregroundColor(.white)
.padding(.vertical, 6)
.padding(.horizontal, 12)
.background(Color.blue)
.cornerRadius(15)
#else
Text(sendTitle)
#endif
}
}
.padding(.horizontal, 12)
HStack {
Text(typingInfo)
.font(.footnote)
Spacer()
}
.frame(height: 12)
.padding(.horizontal, 14)
.padding(.bottom, 5)
}
ForEach(reactions) { reaction in
Text(reaction.emoji)
.font(.largeTitle)
.position(x: reaction.xPosition, y: reaction.yPosition)
.scaleEffect(reaction.scale)
.opacity(reaction.opacity)
.rotationEffect(.degrees(reaction.rotationAngle))
.onAppear {
withAnimation(.easeOut(duration: reaction.duration)) {
moveReactionUp(reaction: reaction)
}
// Start rotation animation
withAnimation(Animation.linear(duration: reaction.duration).repeatForever(autoreverses: false)) {
startRotation(reaction: reaction)
}
}
}
}
.tryTask { try await setDefaultTitle() }
.tryTask { try await showMessages() }
.tryTask { try await showReactions() }
.tryTask { try await showPresence() }
.tryTask { try await showTypings() }
.tryTask { try await showOccupancy() }
.tryTask { try await showRoomStatus() }
}

func sendButtonAction() {
if newMessage.isEmpty {
Task {
try await sendReaction(type: ReactionType.like.rawValue)
}
} else {
Task {
try await sendMessage()
}
}
}

func setDefaultTitle() async throws {
title = try await "\(room().roomID)"
}

func showMessages() async throws {
for await message in try await room().messages.subscribe(bufferingPolicy: .unbounded) {
withAnimation {
messages.insert(BasicListItem(id: message.timeserial, title: message.clientID, text: message.text), at: 0)
}
}
}

func showReactions() async throws {
for await reaction in try await room().reactions.subscribe(bufferingPolicy: .unbounded) {
withAnimation {
showReaction(reaction.displayedText)
}
}
}

func showPresence() async throws {
for await event in try await room().presence.subscribe(events: [.enter, .leave]) {
withAnimation {
messages.insert(BasicListItem(id: UUID().uuidString, title: "System", text: event.clientID + " \(event.action.displayedText)"), at: 0)
}
}
}

func showTypings() async throws {
for await typing in try await room().typing.subscribe(bufferingPolicy: .unbounded) {
withAnimation {
typingInfo = "Typing: \(typing.currentlyTyping.joined(separator: ", "))..."
Task {
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
withAnimation {
typingInfo = ""
}
}
}
}
}

func showOccupancy() async throws {
for await event in try await room().occupancy.subscribe(bufferingPolicy: .unbounded) {
withAnimation {
occupancyInfo = "Connections: \(event.presenceMembers) (\(event.connections))"
}
}
}

func showRoomStatus() async throws {
for await status in try await room().status.onChange(bufferingPolicy: .unbounded) {
withAnimation {
if status.current == .attaching {
statusInfo = "\(status.current)...".capitalized
} else {
statusInfo = "\(status.current)".capitalized
if status.current == .attached {
Task {
try? await Task.sleep(nanoseconds: 1 * 1_000_000_000)
withAnimation {
statusInfo = ""
}
}
}
}
}
}
}

func sendMessage() async throws {
guard !newMessage.isEmpty else {
return
}
_ = try await room().messages.send(params: .init(text: newMessage))
newMessage = ""
}

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

extension ContentView {
struct Reaction: Identifiable {
let id: UUID
let emoji: String
var xPosition: CGFloat
var yPosition: CGFloat
var scale: CGFloat
var opacity: Double
var rotationAngle: Double // New: stores the current rotation angle
var rotationSpeed: Double // New: stores the random rotation speed
var duration: Double
}

func showReaction(_ emoji: String) {
let screenWidth = screenWidth
let centerX = screenWidth / 2

// Reduce the spread to 1/5th of the screen width
let reducedSpreadRange = screenWidth / 5

// Random x position now has a smaller range, centered around the middle of the screen
let startXPosition = CGFloat.random(in: centerX - reducedSpreadRange ... centerX + reducedSpreadRange)
let randomRotationSpeed = Double.random(in: 30 ... 360) // Random rotation speed
let duration = Double.random(in: 2 ... 4)

let newReaction = Reaction(
id: UUID(),
emoji: emoji,
xPosition: startXPosition,
yPosition: screenHeight - 100,
scale: 1.0,
opacity: 1.0,
rotationAngle: 0, // Initial angle
rotationSpeed: randomRotationSpeed,
duration: duration
)

reactions.append(newReaction)

// Remove the reaction after the animation completes
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
reactions.removeAll { $0.id == newReaction.id }
}
}

func moveReactionUp(reaction: Reaction) {
if let index = reactions.firstIndex(where: { $0.id == reaction.id }) {
reactions[index].yPosition = 0 // Move it to the top of the screen
reactions[index].scale = 0.5 // Shrink
reactions[index].opacity = 0.5 // Fade out
}
}

func startRotation(reaction: Reaction) {
if let index = reactions.firstIndex(where: { $0.id == reaction.id }) {
reactions[index].rotationAngle += 360 // Continuous rotation over time
}
}
}

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

struct MessageBasicView: View {
var item: BasicListItem

var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
HStack {
VStack {
Text("\(item.title):")
.foregroundColor(.blue)
.bold()
Spacer()
}
VStack {
Text(item.text)
Spacer()
}
}
.padding()
#if !os(tvOS)
.listRowSeparator(.hidden)
#endif
}
}

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

#Preview {
ContentView()
}

extension PresenceEventType {
var displayedText: String {
switch self {
case .enter:
"has entered the room"
case .leave:
"has left the room"
case .present:
"has presented at the room"
case .update:
"has updated presence"
}
}
}

extension View {
nonisolated func tryTask(priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable () async throws -> Void) -> some View {
task(priority: priority) {
do {
try await action()
} catch {
print("Action can't be performed: \(error)") // TODO: replace with logger (+ message to the user?)
}
}
}
}

0 comments on commit 24cd34b

Please sign in to comment.