mirror of
git://jb55.com/damus
synced 2024-09-30 00:40:45 +00:00
Live Music & Generic Statuses
Changelog-Added: Added live music statuses Changelog-Added: Added generic user statuses
This commit is contained in:
parent
59cf8056bd
commit
0338297bfe
@ -163,11 +163,14 @@
|
||||
4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90548A2A6AEDEE00811EEC /* NdbNote.swift */; };
|
||||
4C5D5C9A2A6AF8F80024563C /* NdbTagIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9054882A6AED4700811EEC /* NdbTagIterator.swift */; };
|
||||
4C5D5C9D2A6B2CB40024563C /* AsciiCharacter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5D5C9C2A6B2CB40024563C /* AsciiCharacter.swift */; };
|
||||
4C5E54032A9522F600FF6E60 /* UserStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5E54022A9522F600FF6E60 /* UserStatus.swift */; };
|
||||
4C5E54062A9671F800FF6E60 /* UserStatusSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5E54052A9671F800FF6E60 /* UserStatusSheet.swift */; };
|
||||
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9113283D694D0052CD1C /* FollowTarget.swift */; };
|
||||
4C5F9116283D855D0052CD1C /* EventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9115283D855D0052CD1C /* EventsModel.swift */; };
|
||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9117283D88E40052CD1C /* FollowingModel.swift */; };
|
||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C63334F283D40E500B1C9C3 /* HomeModel.swift */; };
|
||||
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C633351283D419F00B1C9C3 /* SignalModel.swift */; };
|
||||
4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64305B2A945AFF00B0C0E9 /* MusicController.swift */; };
|
||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
|
||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
|
||||
@ -339,6 +342,7 @@
|
||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; };
|
||||
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
|
||||
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
|
||||
4CF38C882A9442DC00BE01B6 /* UserStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */; };
|
||||
4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFD502E2A2DA45800A229DB /* MediaView.swift */; };
|
||||
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; };
|
||||
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; };
|
||||
@ -697,11 +701,14 @@
|
||||
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
|
||||
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
||||
4C5D5C9C2A6B2CB40024563C /* AsciiCharacter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsciiCharacter.swift; sourceTree = "<group>"; };
|
||||
4C5E54022A9522F600FF6E60 /* UserStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatus.swift; sourceTree = "<group>"; };
|
||||
4C5E54052A9671F800FF6E60 /* UserStatusSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusSheet.swift; sourceTree = "<group>"; };
|
||||
4C5F9113283D694D0052CD1C /* FollowTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowTarget.swift; sourceTree = "<group>"; };
|
||||
4C5F9115283D855D0052CD1C /* EventsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsModel.swift; sourceTree = "<group>"; };
|
||||
4C5F9117283D88E40052CD1C /* FollowingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingModel.swift; sourceTree = "<group>"; };
|
||||
4C63334F283D40E500B1C9C3 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; };
|
||||
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
|
||||
4C64305B2A945AFF00B0C0E9 /* MusicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicController.swift; sourceTree = "<group>"; };
|
||||
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
|
||||
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
|
||||
4C684A542A7E91FE005E6031 /* LongPostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPostTests.swift; sourceTree = "<group>"; };
|
||||
@ -894,6 +901,7 @@
|
||||
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
|
||||
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
|
||||
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
|
||||
4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusView.swift; sourceTree = "<group>"; };
|
||||
4CFD502E2A2DA45800A229DB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
|
||||
@ -1231,6 +1239,25 @@
|
||||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C5E54042A95232A00FF6E60 /* Status */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C64305A2A945AF200B0C0E9 /* Music */,
|
||||
4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */,
|
||||
4C5E54022A9522F600FF6E60 /* UserStatus.swift */,
|
||||
4C5E54052A9671F800FF6E60 /* UserStatusSheet.swift */,
|
||||
);
|
||||
path = Status;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C64305A2A945AF200B0C0E9 /* Music */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C64305B2A945AFF00B0C0E9 /* MusicController.swift */,
|
||||
);
|
||||
path = Music;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C687C2A2A6058450092C550 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1635,6 +1662,7 @@
|
||||
4CE4F9DF285287A000C00DD9 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C5E54042A95232A00FF6E60 /* Status */,
|
||||
4C687C2A2A6058450092C550 /* Search */,
|
||||
4C7D09702A0AEF4C00943473 /* Gradients */,
|
||||
31D2E846295218AF006D67F8 /* Shimmer.swift */,
|
||||
@ -2293,6 +2321,7 @@
|
||||
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */,
|
||||
F71694F82A6983AF001F4053 /* GrayGradient.swift in Sources */,
|
||||
4C1D4FB12A7958E60024F453 /* VersionInfo.swift in Sources */,
|
||||
4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */,
|
||||
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */,
|
||||
F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
@ -2307,6 +2336,7 @@
|
||||
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
||||
4C5E54032A9522F600FF6E60 /* UserStatus.swift in Sources */,
|
||||
4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */,
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
|
||||
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */,
|
||||
@ -2368,6 +2398,7 @@
|
||||
4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */,
|
||||
4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */,
|
||||
4CA352A02A76AE80003BB08B /* Notify.swift in Sources */,
|
||||
4CF38C882A9442DC00BE01B6 /* UserStatusView.swift in Sources */,
|
||||
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
|
||||
4C1253582A76C9060004F4B8 /* PresentSheetNotify.swift in Sources */,
|
||||
4C363A962827096D006E126D /* PostBlock.swift in Sources */,
|
||||
@ -2376,6 +2407,7 @@
|
||||
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */,
|
||||
4C06670E28FDEAA000038D2A /* utf8.c in Sources */,
|
||||
4C3EA66D28FF782800C48A62 /* amount.c in Sources */,
|
||||
4C5E54062A9671F800FF6E60 /* UserStatusSheet.swift in Sources */,
|
||||
F71694F42A6732B7001F4053 /* GradientFollowButton.swift in Sources */,
|
||||
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
|
||||
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */,
|
||||
|
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user