import ActivityKit import SwiftUI import WidgetKit // Mantine v8 avatar tint colors (shade-6) — exact hex values, desktop parity private let mantineTints: [Color] = [ Color(red: 34/255, green: 139/255, blue: 230/255), // #228be6 blue Color(red: 21/255, green: 170/255, blue: 191/255), // #15aabf cyan Color(red: 190/255, green: 75/255, blue: 219/255), // #be4bdb grape Color(red: 64/255, green: 192/255, blue: 87/255), // #40c057 green Color(red: 76/255, green: 110/255, blue: 245/255), // #4c6ef5 indigo Color(red: 130/255, green: 201/255, blue: 30/255), // #82c91e lime Color(red: 253/255, green: 126/255, blue: 20/255), // #fd7e14 orange Color(red: 230/255, green: 73/255, blue: 128/255), // #e64980 pink Color(red: 250/255, green: 82/255, blue: 82/255), // #fa5252 red Color(red: 18/255, green: 184/255, blue: 134/255), // #12b886 teal Color(red: 121/255, green: 80/255, blue: 242/255), // #7950f2 violet ] private let appGroupID = "group.com.rosetta.dev" @main struct CallLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: CallActivityAttributes.self) { context in // MARK: - Lock Screen HStack(spacing: 14) { avatarView(context: context, size: 48, fontSize: 18) VStack(alignment: .leading, spacing: 3) { Text(context.attributes.peerName) .font(.system(size: 17, weight: .semibold)) .foregroundColor(.white) .lineLimit(1) HStack(spacing: 6) { Image(systemName: "lock.fill") .font(.system(size: 10)) .foregroundColor(Color(white: 0.45)) if context.state.isActive { Text(fmt(context.state.durationSec)) .font(.system(size: 14, weight: .medium).monospacedDigit()) .foregroundColor(Color(white: 0.55)) } else { Text("Connecting...") .font(.system(size: 14, weight: .medium)) .foregroundColor(Color(white: 0.55)) } if context.state.isMuted { Image(systemName: "mic.slash.fill") .font(.system(size: 11)) .foregroundColor(.red) } } } Spacer() ZStack { Circle().fill(Color.red) Image(systemName: "phone.down.fill") .font(.system(size: 15, weight: .semibold)) .foregroundColor(.white) } .frame(width: 40, height: 40) } .padding(.horizontal, 20) .padding(.vertical, 14) .activityBackgroundTint(Color(white: 0.08)) } dynamicIsland: { context in DynamicIsland { // MARK: - Expanded DynamicIslandExpandedRegion(.leading) { avatarView(context: context, size: 44, fontSize: 15) .padding(.leading, 6) .padding(.top, 4) } DynamicIslandExpandedRegion(.trailing) { VStack(spacing: 2) { if context.state.isActive { Text(fmt(context.state.durationSec)) .font(.system(size: 16, weight: .bold).monospacedDigit()) .foregroundColor(.green) } else { Text("...") .font(.system(size: 16, weight: .bold)) .foregroundColor(.orange) } if context.state.isMuted { Image(systemName: "mic.slash.fill") .font(.system(size: 11)) .foregroundColor(.red) } } .padding(.trailing, 6) .padding(.top, 4) } DynamicIslandExpandedRegion(.center) { Text(context.attributes.peerName) .font(.system(size: 16, weight: .semibold)) .foregroundColor(.white) .lineLimit(1) .padding(.top, 4) } DynamicIslandExpandedRegion(.bottom) { HStack(spacing: 5) { Circle() .fill(context.state.isActive ? Color.green : Color.orange) .frame(width: 6, height: 6) Image(systemName: "lock.fill") .font(.system(size: 9)) .foregroundColor(Color(white: 0.4)) Text("Rosetta · E2E encrypted") .font(.system(size: 11, weight: .medium)) .foregroundColor(Color(white: 0.4)) } .padding(.top, 4) } } compactLeading: { avatarView(context: context, size: 26, fontSize: 10) } compactTrailing: { if context.state.isActive { Text(fmt(context.state.durationSec)) .font(.system(size: 13, weight: .semibold).monospacedDigit()) .foregroundColor(.green) } else { Image(systemName: "phone.fill") .font(.system(size: 12)) .foregroundColor(.green) } } minimal: { Image(systemName: "phone.fill") .font(.system(size: 11)) .foregroundColor(.green) } } } // MARK: - Avatar @ViewBuilder private func avatarView(context: ActivityViewContext, size: CGFloat, fontSize: CGFloat) -> some View { if let img = loadSharedAvatar() { Image(uiImage: img) .resizable() .scaledToFill() .frame(width: size, height: size) .clipShape(Circle()) } else { let idx = min(context.attributes.colorIndex, mantineTints.count - 1) ZStack { Circle().fill(mantineTints[max(0, idx)]) Text(makeInitials(context.attributes.peerName, context.attributes.peerPublicKey)) .font(.system(size: fontSize, weight: .bold)) .foregroundColor(.white) } .frame(width: size, height: size) } } private func loadSharedAvatar() -> UIImage? { guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)? .appendingPathComponent("call_avatar.jpg"), let data = try? Data(contentsOf: url), let img = UIImage(data: data) else { return nil } return img } // MARK: - Helpers private func fmt(_ sec: Int) -> String { String(format: "%d:%02d", sec / 60, sec % 60) } private func makeInitials(_ name: String, _ pubKey: String) -> String { let words = name.trimmingCharacters(in: .whitespaces) .split(whereSeparator: { $0.isWhitespace }) .filter { !$0.isEmpty } switch words.count { case 0: return pubKey.isEmpty ? "?" : String(pubKey.prefix(2)).uppercased() case 1: return String(words[0].prefix(2)).uppercased() default: return "\(words[0].prefix(1))\(words[1].prefix(1))".uppercased() } } }