mirror of
				https://github.com/yattee/yattee.git
				synced 2025-10-31 04:31:54 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			261 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			261 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
| import SDWebImageSwiftUI
 | |
| import SwiftUI
 | |
| 
 | |
| struct CommentView: View {
 | |
|     let comment: Comment
 | |
|     @Binding var repliesID: Comment.ID?
 | |
| 
 | |
|     @State private var subscribed = false
 | |
| 
 | |
|     #if os(iOS)
 | |
|         @Environment(\.horizontalSizeClass) private var horizontalSizeClass
 | |
|     #endif
 | |
| 
 | |
|     @Environment(\.colorScheme) private var colorScheme
 | |
|     @Environment(\.navigationStyle) private var navigationStyle
 | |
| 
 | |
|     @ObservedObject private var comments = CommentsModel.shared
 | |
|     var subscriptions = SubscribedChannelsModel.shared
 | |
| 
 | |
|     var body: some View {
 | |
|         VStack(alignment: .leading) {
 | |
|             HStack(spacing: 10) {
 | |
|                 HStack(spacing: 10) {
 | |
|                     ZStack(alignment: .bottomTrailing) {
 | |
|                         authorAvatar
 | |
| 
 | |
|                         if subscribed {
 | |
|                             Image(systemName: "star.circle.fill")
 | |
|                             #if os(tvOS)
 | |
|                                 .background(Color.background(scheme: colorScheme))
 | |
|                             #else
 | |
|                                 .background(Color.background)
 | |
|                             #endif
 | |
|                                 .clipShape(Circle())
 | |
|                                 .foregroundColor(.secondary)
 | |
|                         }
 | |
|                     }
 | |
|                     .onAppear {
 | |
|                         subscribed = subscriptions.isSubscribing(comment.channel.id)
 | |
|                     }
 | |
| 
 | |
|                     authorAndTime
 | |
|                 }
 | |
|                 .contextMenu {
 | |
|                     Button(action: openChannelAction) {
 | |
|                         Label("\(comment.channel.name) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 Spacer()
 | |
| 
 | |
|                 Group {
 | |
|                     #if os(iOS)
 | |
|                         if horizontalSizeClass == .regular {
 | |
|                             Group {
 | |
|                                 statusIcons
 | |
|                                 likes
 | |
|                             }
 | |
|                         } else {
 | |
|                             VStack(alignment: .trailing, spacing: 8) {
 | |
|                                 likes
 | |
|                                 statusIcons
 | |
|                             }
 | |
|                         }
 | |
|                     #else
 | |
|                         statusIcons
 | |
|                         likes
 | |
|                     #endif
 | |
|                 }
 | |
|             }
 | |
|             #if os(tvOS)
 | |
|             .font(.system(size: 25).bold())
 | |
|             #else
 | |
|             .font(.system(size: 15))
 | |
|             #endif
 | |
| 
 | |
|             Group {
 | |
|                 commentText
 | |
| 
 | |
|                 if comment.hasReplies {
 | |
|                     HStack(spacing: repliesButtonStackSpacing) {
 | |
|                         repliesButton
 | |
| 
 | |
|                         ProgressView()
 | |
|                             .scaleEffect(Constants.progressViewScale, anchor: .center)
 | |
|                             .opacity(repliesID == comment.id && !comments.repliesLoaded ? 1 : 0)
 | |
|                             .frame(maxHeight: 0)
 | |
|                     }
 | |
| 
 | |
|                     if comment.id == repliesID {
 | |
|                         repliesList
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         #if os(tvOS)
 | |
|         .padding(.horizontal, 20)
 | |
|         #endif
 | |
|         .padding(.bottom, 10)
 | |
|     }
 | |
| 
 | |
|     private var authorAvatar: some View {
 | |
|         WebImage(url: URL(string: comment.authorAvatarURL), options: [.lowPriority])
 | |
|             .resizable()
 | |
|             .placeholder {
 | |
|                 Rectangle().fill(Color("PlaceholderColor"))
 | |
|             }
 | |
|             .retryOnAppear(true)
 | |
|             .indicator(.activity)
 | |
|             .mask(RoundedRectangle(cornerRadius: 60))
 | |
|         #if os(tvOS)
 | |
|             .frame(width: 80, height: 80, alignment: .leading)
 | |
|             .focusable()
 | |
|         #else
 | |
|             .frame(width: 45, height: 45, alignment: .leading)
 | |
|         #endif
 | |
|     }
 | |
| 
 | |
|     private var authorAndTime: some View {
 | |
|         VStack(alignment: .leading) {
 | |
|             Text(comment.author)
 | |
|             #if os(tvOS)
 | |
|                 .font(.system(size: 30).bold())
 | |
|             #else
 | |
|                 .font(.system(size: 14).bold())
 | |
|             #endif
 | |
| 
 | |
|             Text(comment.time)
 | |
|                 .font(.caption2)
 | |
|                 .foregroundColor(.secondary)
 | |
|         }
 | |
|         .lineLimit(1)
 | |
|     }
 | |
| 
 | |
|     private var statusIcons: some View {
 | |
|         HStack(spacing: 15) {
 | |
|             if comment.pinned {
 | |
|                 Image(systemName: "pin.fill")
 | |
|             }
 | |
|             if comment.hearted {
 | |
|                 Image(systemName: "heart.fill")
 | |
|             }
 | |
|         }
 | |
|         #if !os(tvOS)
 | |
|         .font(.system(size: 12))
 | |
|         #endif
 | |
|         .foregroundColor(.secondary)
 | |
|     }
 | |
| 
 | |
|     private var likes: some View {
 | |
|         Group {
 | |
|             if comment.likeCount > 0 {
 | |
|                 HStack(spacing: 5) {
 | |
|                     Image(systemName: "hand.thumbsup")
 | |
|                     Text("\(comment.likeCount.formattedAsAbbreviation())")
 | |
|                 }
 | |
|                 #if !os(tvOS)
 | |
|                 .font(.system(size: 12))
 | |
|                 #endif
 | |
|             }
 | |
|         }
 | |
|         .foregroundColor(.secondary)
 | |
|     }
 | |
| 
 | |
|     private var repliesButton: some View {
 | |
|         Button {
 | |
|             repliesID = repliesID == comment.id ? nil : comment.id
 | |
| 
 | |
|             guard !repliesID.isNil, !comment.repliesPage.isNil else {
 | |
|                 return
 | |
|             }
 | |
| 
 | |
|             comments.loadReplies(page: comment.repliesPage!)
 | |
|         } label: {
 | |
|             HStack(spacing: 5) {
 | |
|                 Image(systemName: repliesID == comment.id ? "arrow.turn.left.up" : "arrow.turn.right.down")
 | |
|                 Text("Replies")
 | |
|             }
 | |
|             #if os(tvOS)
 | |
|             .padding(10)
 | |
|             #endif
 | |
|         }
 | |
|         .buttonStyle(.plain)
 | |
|         .padding(.vertical, 2)
 | |
|         #if os(tvOS)
 | |
|             .padding(.leading, 5)
 | |
|         #else
 | |
|             .font(.system(size: 13))
 | |
|             .foregroundColor(.secondary)
 | |
|         #endif
 | |
|     }
 | |
| 
 | |
|     private var repliesButtonStackSpacing: Double {
 | |
|         #if os(tvOS)
 | |
|             24
 | |
|         #elseif os(iOS)
 | |
|             4
 | |
|         #else
 | |
|             2
 | |
|         #endif
 | |
|     }
 | |
| 
 | |
|     private var repliesList: some View {
 | |
|         Group {
 | |
|             let last = comments.replies.last
 | |
|             ForEach(comments.replies) { comment in
 | |
|                 Self(comment: comment, repliesID: $repliesID)
 | |
|                 #if os(tvOS)
 | |
|                     .focusable()
 | |
|                 #endif
 | |
| 
 | |
|                 if comment != last {
 | |
|                     Divider()
 | |
|                         .padding(.vertical, 5)
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         .padding(.leading, 22)
 | |
|     }
 | |
| 
 | |
|     private var commentText: some View {
 | |
|         Group {
 | |
|             let text = Text(comment.text)
 | |
|             #if os(macOS)
 | |
|                 .font(.system(size: 14))
 | |
|             #elseif os(iOS)
 | |
|                 .font(.system(size: 15))
 | |
|             #endif
 | |
|                 .lineSpacing(3)
 | |
|                 .fixedSize(horizontal: false, vertical: true)
 | |
| 
 | |
|             if #available(iOS 15.0, macOS 12.0, *) {
 | |
|                 text
 | |
|                 #if !os(tvOS)
 | |
|                 .textSelection(.enabled)
 | |
|                 #endif
 | |
|             } else {
 | |
|                 text
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private func openChannelAction() {
 | |
|         NavigationModel.shared.openChannel(
 | |
|             comment.channel,
 | |
|             navigationStyle: navigationStyle
 | |
|         )
 | |
|     }
 | |
| }
 | |
| 
 | |
| struct CommentView_Previews: PreviewProvider {
 | |
|     static var fixture: Comment {
 | |
|         Comment.fixture
 | |
|     }
 | |
| 
 | |
|     static var previews: some View {
 | |
|         CommentView(comment: fixture, repliesID: .constant(fixture.id))
 | |
|             .padding(5)
 | |
|     }
 | |
| }
 | 
