Live Music & Generic Statuses
Changelog-Added: Added live music statuses Changelog-Added: Added generic user statuses
This commit is contained in:
48
damus/Components/Status/Music/MusicController.swift
Normal file
48
damus/Components/Status/Music/MusicController.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// MusicController.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-08-21.
|
||||
//
|
||||
import SwiftUI
|
||||
import MediaPlayer
|
||||
|
||||
enum MusicState {
|
||||
case playback_state(MPMusicPlaybackState)
|
||||
case song(MPMediaItem?)
|
||||
}
|
||||
|
||||
class MusicController {
|
||||
let player: MPMusicPlayerController
|
||||
|
||||
let onChange: (MusicState) -> ()
|
||||
|
||||
init(onChange: @escaping (MusicState) -> ()) {
|
||||
player = .systemMusicPlayer
|
||||
|
||||
player.beginGeneratingPlaybackNotifications()
|
||||
|
||||
self.onChange = onChange
|
||||
|
||||
print("Playback State: \(player.playbackState)")
|
||||
print("Now Playing Item: \(player.nowPlayingItem?.title ?? "None")")
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.songChanged(notification:)), name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: player)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.playbackStatusChanged(notification:)), name: .MPMusicPlayerControllerPlaybackStateDidChange, object: player)
|
||||
}
|
||||
|
||||
deinit {
|
||||
print("deinit musiccontroller")
|
||||
}
|
||||
|
||||
@objc
|
||||
func songChanged(notification: Notification) {
|
||||
onChange(.song(player.nowPlayingItem))
|
||||
}
|
||||
|
||||
@objc
|
||||
func playbackStatusChanged(notification: Notification) {
|
||||
onChange(.playback_state(player.playbackState))
|
||||
}
|
||||
}
|
||||
141
damus/Components/Status/UserStatus.swift
Normal file
141
damus/Components/Status/UserStatus.swift
Normal file
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// UserStatus.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-08-22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
struct Song {
|
||||
let started_playing: Date
|
||||
let content: String
|
||||
|
||||
|
||||
}
|
||||
|
||||
struct UserStatus {
|
||||
let type: UserStatusType
|
||||
let expires_at: Date?
|
||||
let content: String
|
||||
|
||||
func to_note(keypair: FullKeypair) -> NostrEvent? {
|
||||
return make_user_status_note(status: self, keypair: keypair)
|
||||
}
|
||||
|
||||
init(type: UserStatusType, expires_at: Date?, content: String) {
|
||||
self.type = type
|
||||
self.expires_at = expires_at
|
||||
self.content = content
|
||||
}
|
||||
|
||||
init?(ev: NostrEvent) {
|
||||
guard let tag = ev.referenced_params.just_one() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let str = tag.param.string()
|
||||
if str == "general" {
|
||||
self.type = .general
|
||||
} else if str == "music" {
|
||||
self.type = .music
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let tag = ev.tags.first(where: { t in t.count >= 2 && t[0].matches_str("expiration") }),
|
||||
tag.count == 2,
|
||||
let expires = UInt32(tag[1].string())
|
||||
{
|
||||
self.expires_at = Date(timeIntervalSince1970: TimeInterval(expires))
|
||||
} else {
|
||||
self.expires_at = nil
|
||||
}
|
||||
|
||||
self.content = ev.content
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum UserStatusType: String {
|
||||
case music
|
||||
case general
|
||||
|
||||
}
|
||||
|
||||
class UserStatusModel: ObservableObject {
|
||||
@Published var general: UserStatus?
|
||||
@Published var music: UserStatus?
|
||||
|
||||
func update_status(_ s: UserStatus) {
|
||||
switch s.type {
|
||||
case .music:
|
||||
self.music = s
|
||||
case .general:
|
||||
self.general = s
|
||||
}
|
||||
}
|
||||
|
||||
var _playing_enabled: Bool
|
||||
var playing_enabled: Bool {
|
||||
set {
|
||||
var new_val = newValue
|
||||
|
||||
if newValue {
|
||||
MPMediaLibrary.requestAuthorization { astatus in
|
||||
switch astatus {
|
||||
case .notDetermined: new_val = false
|
||||
case .denied: new_val = false
|
||||
case .restricted: new_val = false
|
||||
case .authorized: new_val = true
|
||||
@unknown default:
|
||||
new_val = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if new_val != playing_enabled {
|
||||
_playing_enabled = new_val
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
get {
|
||||
return _playing_enabled
|
||||
}
|
||||
}
|
||||
|
||||
init(playing: UserStatus? = nil, status: UserStatus? = nil) {
|
||||
self.general = status
|
||||
self.music = playing
|
||||
self._playing_enabled = false
|
||||
self.playing_enabled = false
|
||||
}
|
||||
|
||||
static var current_track: String? {
|
||||
let player = MPMusicPlayerController.systemMusicPlayer
|
||||
guard let nowPlayingItem = player.nowPlayingItem else { return nil }
|
||||
return nowPlayingItem.title
|
||||
}
|
||||
}
|
||||
|
||||
func make_user_status_note(status: UserStatus, keypair: FullKeypair, expiry: Date? = nil) -> NostrEvent?
|
||||
{
|
||||
var tags: [[String]] = [ ["d", status.type.rawValue] ]
|
||||
|
||||
if let expiry {
|
||||
tags.append(["expiration", String(UInt32(expiry.timeIntervalSince1970))])
|
||||
} else if let expiry = status.expires_at {
|
||||
tags.append(["expiration", String(UInt32(expiry.timeIntervalSince1970))])
|
||||
}
|
||||
|
||||
let kind = NostrKind.status.rawValue
|
||||
guard let ev = NostrEvent(content: status.content, keypair: keypair.to_keypair(), kind: kind, tags: tags) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
116
damus/Components/Status/UserStatusSheet.swift
Normal file
116
damus/Components/Status/UserStatusSheet.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// UserStatusSheet.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-08-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum StatusDuration: String, CaseIterable {
|
||||
case never = "Never"
|
||||
case thirty_mins = "30 Minutes"
|
||||
case hour = "1 Hour"
|
||||
case four_hours = "4 Hours"
|
||||
case day = "1 Day"
|
||||
case week = "1 Week"
|
||||
|
||||
var expiration: Date? {
|
||||
switch self {
|
||||
case .never:
|
||||
return nil
|
||||
case .thirty_mins:
|
||||
return Date.now.addingTimeInterval(60 * 30)
|
||||
case .hour:
|
||||
return Date.now.addingTimeInterval(60 * 60)
|
||||
case .four_hours:
|
||||
return Date.now.addingTimeInterval(60 * 60 * 4)
|
||||
case .day:
|
||||
return Date.now.addingTimeInterval(60 * 60 * 24)
|
||||
case .week:
|
||||
return Date.now.addingTimeInterval(60 * 60 * 24 * 7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserStatusSheet: View {
|
||||
let postbox: PostBox
|
||||
let keypair: Keypair
|
||||
|
||||
@State var duration: StatusDuration = .never
|
||||
@ObservedObject var status: UserStatusModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var status_binding: Binding<String> {
|
||||
Binding(get: {
|
||||
status.general?.content ?? ""
|
||||
}, set: { v in
|
||||
status.general = UserStatus(type: .general, expires_at: duration.expiration, content: v)
|
||||
})
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Set Status")
|
||||
.font(.largeTitle)
|
||||
|
||||
TextField(text: status_binding, label: {
|
||||
Text("📋 Working")
|
||||
})
|
||||
|
||||
HStack {
|
||||
Text("Clear status")
|
||||
|
||||
Spacer()
|
||||
|
||||
Picker("Duration", selection: $duration) {
|
||||
ForEach(StatusDuration.allCases, id: \.self) { d in
|
||||
Text("\(d.rawValue)")
|
||||
.tag(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle(isOn: $status.playing_enabled, label: {
|
||||
Text("Broadcast music playing on Apple Music")
|
||||
})
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text("Cancel")
|
||||
})
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
guard let status = self.status.general,
|
||||
let kp = keypair.to_full(),
|
||||
let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
postbox.send(ev)
|
||||
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text("Save")
|
||||
})
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
}
|
||||
.padding([.top], 30)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(30)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct UserStatusSheet_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserStatusSheet(postbox: PostBox(pool: RelayPool()), keypair: Keypair(pubkey: .empty, privkey: nil), status: .init())
|
||||
}
|
||||
}
|
||||
37
damus/Components/Status/UserStatusView.swift
Normal file
37
damus/Components/Status/UserStatusView.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// UserStatus.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-08-21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MediaPlayer
|
||||
|
||||
|
||||
struct UserStatusView: View {
|
||||
@ObservedObject var status: UserStatusModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let general = status.general {
|
||||
Text(verbatim: "\(general.content)")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout.italic())
|
||||
}
|
||||
|
||||
if let playing = status.music {
|
||||
Text(verbatim: "🎵\(playing.content)")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout.italic())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct UserStatusView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserStatusView(status: .init())
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import MediaPlayer
|
||||
|
||||
struct TimestampedProfile {
|
||||
let profile: Profile
|
||||
@@ -30,6 +31,7 @@ enum Sheets: Identifiable {
|
||||
case zap(ZapSheet)
|
||||
case select_wallet(SelectWallet)
|
||||
case filter
|
||||
case user_status
|
||||
case suggestedUsers
|
||||
|
||||
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
||||
@@ -43,6 +45,7 @@ enum Sheets: Identifiable {
|
||||
var id: String {
|
||||
switch self {
|
||||
case .report: return "report"
|
||||
case .user_status: return "user_status"
|
||||
case .post(let action): return "post-" + (action.ev?.id.hex() ?? "")
|
||||
case .event(let ev): return "event-" + ev.id.hex()
|
||||
case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id)
|
||||
@@ -315,6 +318,8 @@ struct ContentView: View {
|
||||
MaybeReportView(target: target)
|
||||
case .post(let action):
|
||||
PostView(action: action, damus_state: damus_state!)
|
||||
case .user_status:
|
||||
UserStatusSheet(postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
||||
case .event:
|
||||
EventDetailView()
|
||||
case .zap(let zapsheet):
|
||||
@@ -647,14 +652,32 @@ struct ContentView: View {
|
||||
muted_threads: MutedThreadsManager(keypair: keypair),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: self.navigationCoordinator,
|
||||
user_search_cache: user_search_cache
|
||||
user_search_cache: user_search_cache,
|
||||
music: MusicController(onChange: music_changed)
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
pool.connect()
|
||||
}
|
||||
|
||||
|
||||
func music_changed(_ state: MusicState) {
|
||||
guard let damus_state else { return }
|
||||
switch state {
|
||||
case .playback_state:
|
||||
break
|
||||
case .song(let song):
|
||||
guard let song, let kp = damus_state.keypair.to_full() else { return }
|
||||
|
||||
let pdata = damus_state.profiles.profile_data(damus_state.pubkey)
|
||||
|
||||
let music = UserStatus(type: .music, expires_at: Date.now.addingTimeInterval(song.playbackDuration), content: "\(song.title ?? "Unknown") - \(song.artist ?? "Unknown")")
|
||||
pdata.status.music = music
|
||||
|
||||
guard let ev = music.to_note(keypair: kp) else { return }
|
||||
damus_state.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
@@ -744,7 +767,6 @@ func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [Nos
|
||||
|
||||
|
||||
func setup_notifications() {
|
||||
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
let center = UNUserNotificationCenter.current()
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Damus needs access to your camera if you want to upload photos from it</string>
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>Damus needs access to your media library for playback statuses</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
|
||||
</dict>
|
||||
|
||||
@@ -32,7 +32,8 @@ struct DamusState {
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
let user_search_cache: UserSearchCache
|
||||
|
||||
let music: MusicController?
|
||||
|
||||
@discardableResult
|
||||
func add_zap(zap: Zapping) -> Bool {
|
||||
// store generic zap mapping
|
||||
@@ -87,6 +88,8 @@ struct DamusState {
|
||||
muted_threads: MutedThreadsManager(keypair: kp),
|
||||
wallet: WalletModel(settings: UserSettingsStore()),
|
||||
nav: NavigationCoordinator(),
|
||||
user_search_cache: user_search_cache)
|
||||
user_search_cache: user_search_cache,
|
||||
music: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,9 +190,19 @@ class HomeModel {
|
||||
handle_nwc_response(ev, relay: relay_id)
|
||||
case .http_auth:
|
||||
break
|
||||
case .status:
|
||||
handle_status_event(ev)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func handle_status_event(_ ev: NostrEvent) {
|
||||
guard let st = UserStatus(ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.profiles.profile_data(ev.pubkey).status.update_status(st)
|
||||
}
|
||||
|
||||
func handle_nwc_response(_ ev: NostrEvent, relay: String) {
|
||||
Task { @MainActor in
|
||||
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
|
||||
@@ -502,7 +512,7 @@ class HomeModel {
|
||||
func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: String? = nil) {
|
||||
// TODO: separate likes?
|
||||
var home_filter_kinds: [NostrKind] = [
|
||||
.text, .longform, .boost
|
||||
.text, .longform, .boost, .status
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
home_filter_kinds.append(.like)
|
||||
@@ -1401,7 +1411,7 @@ func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @esc
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.profiles.zappers[ptag] = zapper
|
||||
damus_state.profiles.profile_data(ptag).zapper = zapper
|
||||
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: zapper) else {
|
||||
completion(.failed)
|
||||
return
|
||||
|
||||
@@ -24,4 +24,5 @@ enum NostrKind: UInt32, Codable {
|
||||
case nwc_request = 23194
|
||||
case nwc_response = 23195
|
||||
case http_auth = 27235
|
||||
case status = 30315
|
||||
}
|
||||
|
||||
@@ -7,6 +7,36 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class ValidationModel: ObservableObject {
|
||||
@Published var validated: NIP05?
|
||||
|
||||
init() {
|
||||
self.validated = nil
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileDataModel: ObservableObject {
|
||||
@Published var profile: TimestampedProfile?
|
||||
|
||||
init() {
|
||||
self.profile = nil
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileData {
|
||||
var status: UserStatusModel
|
||||
var profile_model: ProfileDataModel
|
||||
var validation_model: ValidationModel
|
||||
var zapper: Pubkey?
|
||||
|
||||
init() {
|
||||
status = .init()
|
||||
profile_model = .init()
|
||||
validation_model = .init()
|
||||
zapper = nil
|
||||
}
|
||||
}
|
||||
|
||||
class Profiles {
|
||||
|
||||
static let db_freshness_threshold: TimeInterval = 24 * 60 * 60
|
||||
@@ -21,10 +51,9 @@ class Profiles {
|
||||
qos: .userInteractive,
|
||||
attributes: .concurrent)
|
||||
|
||||
private var profiles: [Pubkey: TimestampedProfile] = [:]
|
||||
private var validated: [Pubkey: NIP05] = [:]
|
||||
private var profiles: [Pubkey: ProfileData] = [:]
|
||||
|
||||
var nip05_pubkey: [String: Pubkey] = [:]
|
||||
var zappers: [Pubkey: Pubkey] = [:]
|
||||
|
||||
private let database = ProfileDatabase()
|
||||
|
||||
@@ -36,36 +65,40 @@ class Profiles {
|
||||
|
||||
func is_validated(_ pk: Pubkey) -> NIP05? {
|
||||
validated_queue.sync {
|
||||
validated[pk]
|
||||
self.profile_data(pk).validation_model.validated
|
||||
}
|
||||
}
|
||||
|
||||
func invalidate_nip05(_ pk: Pubkey) {
|
||||
validated_queue.async(flags: .barrier) {
|
||||
self.validated.removeValue(forKey: pk)
|
||||
self.profile_data(pk).validation_model.validated = nil
|
||||
}
|
||||
}
|
||||
|
||||
func set_validated(_ pk: Pubkey, nip05: NIP05?) {
|
||||
validated_queue.async(flags: .barrier) {
|
||||
self.validated[pk] = nip05
|
||||
self.profile_data(pk).validation_model.validated = nip05
|
||||
}
|
||||
}
|
||||
|
||||
func enumerated() -> EnumeratedSequence<[Pubkey: TimestampedProfile]> {
|
||||
return profiles_queue.sync {
|
||||
return profiles.enumerated()
|
||||
func profile_data(_ pubkey: Pubkey) -> ProfileData {
|
||||
guard let data = profiles[pubkey] else {
|
||||
let data = ProfileData()
|
||||
profiles[pubkey] = data
|
||||
return data
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
func lookup_zapper(pubkey: Pubkey) -> Pubkey? {
|
||||
zappers[pubkey]
|
||||
profile_data(pubkey).zapper
|
||||
}
|
||||
|
||||
func add(id: Pubkey, profile: TimestampedProfile) {
|
||||
profiles_queue.async(flags: .barrier) {
|
||||
let old_timestamped_profile = self.profiles[id]
|
||||
self.profiles[id] = profile
|
||||
let old_timestamped_profile = self.profile_data(id).profile_model.profile
|
||||
self.profile_data(id).profile_model.profile = profile
|
||||
self.user_search_cache.updateProfile(id: id, profiles: self, oldProfile: old_timestamped_profile?.profile, newProfile: profile.profile)
|
||||
}
|
||||
|
||||
@@ -81,21 +114,21 @@ class Profiles {
|
||||
func lookup(id: Pubkey) -> Profile? {
|
||||
var profile: Profile?
|
||||
profiles_queue.sync {
|
||||
profile = profiles[id]?.profile
|
||||
profile = self.profile_data(id).profile_model.profile?.profile
|
||||
}
|
||||
return profile ?? database.get(id: id)
|
||||
}
|
||||
|
||||
func lookup_with_timestamp(id: Pubkey) -> TimestampedProfile? {
|
||||
profiles_queue.sync {
|
||||
return profiles[id]
|
||||
return self.profile_data(id).profile_model.profile
|
||||
}
|
||||
}
|
||||
|
||||
func has_fresh_profile(id: Pubkey) -> Bool {
|
||||
var profile: Profile?
|
||||
profiles_queue.sync {
|
||||
profile = profiles[id]?.profile
|
||||
profile = self.profile_data(id).profile_model.profile?.profile
|
||||
}
|
||||
if profile != nil {
|
||||
return true
|
||||
@@ -113,6 +146,6 @@ class Profiles {
|
||||
|
||||
|
||||
func invalidate_zapper_cache(pubkey: Pubkey, profiles: Profiles, lnurl: LNUrls) {
|
||||
profiles.zappers.removeValue(forKey: pubkey)
|
||||
profiles.profile_data(pubkey).zapper = nil
|
||||
lnurl.endpoints.removeValue(forKey: pubkey)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ struct EventTop: View {
|
||||
Spacer()
|
||||
EventMenuContext(damus: state, event: event)
|
||||
}
|
||||
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,11 @@ struct EventProfile: View {
|
||||
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
|
||||
}
|
||||
}
|
||||
|
||||
EventProfileName(pubkey: pubkey, profile: profile, damus: damus_state, size: size)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
EventProfileName(pubkey: pubkey, profile: profile, damus: damus_state, size: size)
|
||||
UserStatusView(status: damus_state.profiles.profile_data(pubkey).status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +93,9 @@ struct EventShell<Content: View>: View {
|
||||
HStack(spacing: 10) {
|
||||
Pfp(is_anon: is_anon)
|
||||
|
||||
VStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
EventTop(state: state, event: event, pubkey: pubkey, is_anon: is_anon)
|
||||
UserStatusView(status: state.profiles.profile_data(pubkey).status)
|
||||
ReplyPart(events: state.events, event: event, privkey: state.keypair.privkey, profiles: state.profiles)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ struct SelectedEventView: View {
|
||||
.padding(.horizontal)
|
||||
.minimumScaleFactor(0.75)
|
||||
.lineLimit(1)
|
||||
|
||||
|
||||
if event_is_reply(event.event_refs(damus.keypair.privkey)) {
|
||||
ReplyDescription(event: event, replying_to: replying_to, profiles: damus.profiles)
|
||||
.padding(.horizontal)
|
||||
|
||||
@@ -55,7 +55,7 @@ struct EventProfileName: View {
|
||||
|
||||
return donation
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
switch current_display_name {
|
||||
|
||||
20
damus/Views/Profile/ProfilePopup.swift
Normal file
20
damus/Views/Profile/ProfilePopup.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// ProfilePopup.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-08-21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProfilePopup: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfilePopup_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProfilePopup()
|
||||
}
|
||||
}
|
||||
@@ -83,23 +83,37 @@ struct SideMenuView: View {
|
||||
|
||||
var TopProfile: some View {
|
||||
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
|
||||
return HStack {
|
||||
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
return VStack(alignment: .leading, spacing: verticalSpacing) {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
if let display_name = profile?.display_name {
|
||||
Text(display_name)
|
||||
.foregroundColor(textColor())
|
||||
.font(.title)
|
||||
.lineLimit(1)
|
||||
}
|
||||
if let name = profile?.name {
|
||||
Text("@" + name)
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
.font(.body)
|
||||
.lineLimit(1)
|
||||
VStack(alignment: .leading) {
|
||||
if let display_name = profile?.display_name {
|
||||
Text(display_name)
|
||||
.foregroundColor(textColor())
|
||||
.font(.title)
|
||||
.lineLimit(1)
|
||||
}
|
||||
if let name = profile?.name {
|
||||
Text("@" + name)
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
.font(.body)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navLabel(title: NSLocalizedString("Set Status", comment: "Sidebar menu label to set user status"), img: "add-reaction")
|
||||
.font(.title2)
|
||||
.foregroundColor(textColor())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.dynamicTypeSize(.xSmall)
|
||||
.onTapGesture {
|
||||
present_sheet(.user_status)
|
||||
}
|
||||
|
||||
UserStatusView(status: damus_state.profiles.profile_data(damus_state.pubkey).status)
|
||||
.dynamicTypeSize(.xSmall)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,17 +204,17 @@ struct SideMenuView: View {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
func navLabel(title: String, img: String) -> some View {
|
||||
Image(img)
|
||||
.tint(DamusColors.adaptableBlack)
|
||||
|
||||
Text(title)
|
||||
.font(.title2)
|
||||
.foregroundColor(textColor())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.dynamicTypeSize(.xSmall)
|
||||
HStack {
|
||||
Image(img)
|
||||
.tint(DamusColors.adaptableBlack)
|
||||
|
||||
Text(title)
|
||||
.font(.title2)
|
||||
.foregroundColor(textColor())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.dynamicTypeSize(.xSmall)
|
||||
}
|
||||
}
|
||||
|
||||
struct SideMenuLabelStyle: LabelStyle {
|
||||
|
||||
Reference in New Issue
Block a user