import ActivityKit import SwiftUI import WidgetKit // Mantine v8 avatar palette (11 colors) — desktop parity private let mantineAvatarTints: [Color] = [ Color(red: 0.133, green: 0.545, blue: 0.902), // blue #228be6 Color(red: 0.082, green: 0.667, blue: 0.749), // cyan #15aabf Color(red: 0.745, green: 0.294, blue: 0.859), // grape #be4bdb Color(red: 0.251, green: 0.753, blue: 0.341), // green #40c057 Color(red: 0.298, green: 0.431, blue: 0.961), // indigo #4c6ef5 Color(red: 0.510, green: 0.788, blue: 0.118), // lime #82c91e Color(red: 0.992, green: 0.494, blue: 0.078), // orange #fd7e14 Color(red: 0.902, green: 0.286, blue: 0.502), // pink #e64980 Color(red: 0.980, green: 0.322, blue: 0.322), // red #fa5252 Color(red: 0.071, green: 0.722, blue: 0.525), // teal #12b886 Color(red: 0.475, green: 0.314, blue: 0.949), // violet #7950f2 ] @main struct CallLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: CallActivityAttributes.self) { context in // MARK: - Lock Screen Banner 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(duration(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(duration(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(duration(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 data = context.attributes.avatarData, let uiImage = UIImage(data: data) { Image(uiImage: uiImage) .resizable() .scaledToFill() .frame(width: size, height: size) .clipShape(Circle()) } else { let idx = context.attributes.colorIndex let color = mantineAvatarTints[idx < mantineAvatarTints.count ? idx : 0] ZStack { Circle().fill(color) Text(initials(from: context.attributes.peerName, publicKey: context.attributes.peerPublicKey)) .font(.system(size: fontSize, weight: .bold)) .foregroundColor(.white) } .frame(width: size, height: size) } } // MARK: - Helpers private func duration(_ sec: Int) -> String { String(format: "%d:%02d", sec / 60, sec % 60) } private func initials(from name: String, publicKey: String) -> String { let words = name.trimmingCharacters(in: .whitespaces) .split(whereSeparator: { $0.isWhitespace }) .filter { !$0.isEmpty } switch words.count { case 0: return publicKey.isEmpty ? "?" : String(publicKey.prefix(2)).uppercased() case 1: return String(words[0].prefix(2)).uppercased() default: let first = words[0].first.map(String.init) ?? "" let second = words[1].first.map(String.init) ?? "" return (first + second).uppercased() } } }