2024-08-19 22:52:04 +00:00
|
|
|
#if os(iOS)
|
|
|
|
import ActiveLabel
|
|
|
|
#endif
|
2021-12-04 19:35:41 +00:00
|
|
|
import SDWebImageSwiftUI
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
struct CommentView: View {
|
|
|
|
let comment: Comment
|
|
|
|
@Binding var repliesID: Comment.ID?
|
2024-08-19 22:52:04 +00:00
|
|
|
var availableWidth: CGFloat
|
2021-12-04 19:35:41 +00:00
|
|
|
|
2021-12-17 19:46:49 +00:00
|
|
|
@State private var subscribed = false
|
|
|
|
|
2021-12-04 19:35:41 +00:00
|
|
|
#if os(iOS)
|
|
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
|
|
#endif
|
2021-12-17 19:46:49 +00:00
|
|
|
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
2021-12-04 19:35:41 +00:00
|
|
|
@Environment(\.navigationStyle) private var navigationStyle
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
@ObservedObject private var comments = CommentsModel.shared
|
2022-12-11 15:15:42 +00:00
|
|
|
var subscriptions = SubscribedChannelsModel.shared
|
2021-12-04 19:35:41 +00:00
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
VStack(alignment: .leading) {
|
2022-11-25 18:31:48 +00:00
|
|
|
HStack(spacing: 10) {
|
2021-12-17 19:46:49 +00:00
|
|
|
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)
|
|
|
|
}
|
2021-12-04 19:35:41 +00:00
|
|
|
|
2021-12-17 19:46:49 +00:00
|
|
|
authorAndTime
|
|
|
|
}
|
|
|
|
.contextMenu {
|
|
|
|
Button(action: openChannelAction) {
|
|
|
|
Label("\(comment.channel.name) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
|
|
|
|
}
|
|
|
|
}
|
2021-12-04 19:35:41 +00:00
|
|
|
|
2021-12-17 19:46:49 +00:00
|
|
|
Spacer()
|
2021-12-04 19:35:41 +00:00
|
|
|
|
2021-12-17 19:46:49 +00:00
|
|
|
Group {
|
|
|
|
#if os(iOS)
|
|
|
|
if horizontalSizeClass == .regular {
|
|
|
|
Group {
|
|
|
|
statusIcons
|
|
|
|
likes
|
2021-12-04 19:35:41 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-12-17 19:46:49 +00:00
|
|
|
VStack(alignment: .trailing, spacing: 8) {
|
|
|
|
likes
|
|
|
|
statusIcons
|
2021-12-04 19:35:41 +00:00
|
|
|
}
|
|
|
|
}
|
2021-12-17 19:46:49 +00:00
|
|
|
#else
|
2021-12-04 19:35:41 +00:00
|
|
|
statusIcons
|
|
|
|
likes
|
2021-12-17 19:46:49 +00:00
|
|
|
#endif
|
|
|
|
}
|
2021-12-04 19:35:41 +00:00
|
|
|
}
|
2021-12-17 19:46:49 +00:00
|
|
|
#if os(tvOS)
|
|
|
|
.font(.system(size: 25).bold())
|
|
|
|
#else
|
|
|
|
.font(.system(size: 15))
|
|
|
|
#endif
|
2021-12-04 19:35:41 +00:00
|
|
|
|
|
|
|
Group {
|
|
|
|
commentText
|
|
|
|
|
|
|
|
if comment.hasReplies {
|
2021-12-05 17:14:49 +00:00
|
|
|
HStack(spacing: repliesButtonStackSpacing) {
|
|
|
|
repliesButton
|
|
|
|
|
|
|
|
ProgressView()
|
2022-12-10 02:01:59 +00:00
|
|
|
.scaleEffect(Constants.progressViewScale, anchor: .center)
|
2021-12-05 17:14:49 +00:00
|
|
|
.opacity(repliesID == comment.id && !comments.repliesLoaded ? 1 : 0)
|
|
|
|
.frame(maxHeight: 0)
|
|
|
|
}
|
2021-12-04 19:35:41 +00:00
|
|
|
|
|
|
|
if comment.id == repliesID {
|
|
|
|
repliesList
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#if os(tvOS)
|
|
|
|
.padding(.horizontal, 20)
|
|
|
|
#endif
|
2022-06-24 23:39:29 +00:00
|
|
|
.padding(.bottom, 10)
|
2021-12-04 19:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private var authorAvatar: some View {
|
2024-02-02 09:08:04 +00:00
|
|
|
WebImage(url: URL(string: comment.authorAvatarURL), options: [.lowPriority])
|
2021-12-04 19:35:41 +00:00
|
|
|
.resizable()
|
|
|
|
.placeholder {
|
|
|
|
Rectangle().fill(Color("PlaceholderColor"))
|
|
|
|
}
|
2021-12-24 19:21:11 +00:00
|
|
|
.retryOnAppear(true)
|
2021-12-04 19:35:41 +00:00
|
|
|
.indicator(.activity)
|
|
|
|
.mask(RoundedRectangle(cornerRadius: 60))
|
|
|
|
#if os(tvOS)
|
2021-12-17 19:46:49 +00:00
|
|
|
.frame(width: 80, height: 80, alignment: .leading)
|
2021-12-04 19:35:41 +00:00
|
|
|
.focusable()
|
2021-12-17 19:46:49 +00:00
|
|
|
#else
|
|
|
|
.frame(width: 45, height: 45, alignment: .leading)
|
2021-12-04 19:35:41 +00:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
private var authorAndTime: some View {
|
|
|
|
VStack(alignment: .leading) {
|
|
|
|
Text(comment.author)
|
2021-12-17 19:46:49 +00:00
|
|
|
#if os(tvOS)
|
|
|
|
.font(.system(size: 30).bold())
|
|
|
|
#else
|
|
|
|
.font(.system(size: 14).bold())
|
|
|
|
#endif
|
2021-12-04 19:35:41 +00:00
|
|
|
|
|
|
|
Text(comment.time)
|
2021-12-17 19:46:49 +00:00
|
|
|
.font(.caption2)
|
2024-08-24 12:21:52 +00:00
|
|
|
#if !os(tvOS)
|
2021-12-04 19:35:41 +00:00
|
|
|
.foregroundColor(.secondary)
|
2024-08-24 12:21:52 +00:00
|
|
|
#endif
|
2021-12-04 19:35:41 +00:00
|
|
|
}
|
|
|
|
.lineLimit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
private var statusIcons: some View {
|
|
|
|
HStack(spacing: 15) {
|
|
|
|
if comment.pinned {
|
|
|
|
Image(systemName: "pin.fill")
|
|
|
|
}
|
|
|
|
if comment.hearted {
|
|
|
|
Image(systemName: "heart.fill")
|
|
|
|
}
|
|
|
|
}
|
2021-12-17 19:46:49 +00:00
|
|
|
#if !os(tvOS)
|
|
|
|
.font(.system(size: 12))
|
|
|
|
#endif
|
2021-12-04 19:35:41 +00:00
|
|
|
.foregroundColor(.secondary)
|
|
|
|
}
|
|
|
|
|
|
|
|
private var likes: some View {
|
|
|
|
Group {
|
|
|
|
if comment.likeCount > 0 {
|
|
|
|
HStack(spacing: 5) {
|
|
|
|
Image(systemName: "hand.thumbsup")
|
|
|
|
Text("\(comment.likeCount.formattedAsAbbreviation())")
|
|
|
|
}
|
2021-12-17 19:46:49 +00:00
|
|
|
#if !os(tvOS)
|
2024-08-24 12:21:52 +00:00
|
|
|
.foregroundColor(.secondary)
|
2021-12-17 19:46:49 +00:00
|
|
|
.font(.system(size: 12))
|
|
|
|
#endif
|
2021-12-04 19:35:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2021-12-05 17:14:49 +00:00
|
|
|
Image(systemName: repliesID == comment.id ? "arrow.turn.left.up" : "arrow.turn.right.down")
|
2021-12-04 19:35:41 +00:00
|
|
|
Text("Replies")
|
|
|
|
}
|
|
|
|
#if os(tvOS)
|
2024-08-24 12:21:52 +00:00
|
|
|
.font(.system(size: 26))
|
|
|
|
.padding(.vertical, 3)
|
2021-12-04 19:35:41 +00:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
.buttonStyle(.plain)
|
2021-12-05 17:14:49 +00:00
|
|
|
.padding(.vertical, 2)
|
2021-12-04 19:35:41 +00:00
|
|
|
#if os(tvOS)
|
|
|
|
.padding(.leading, 5)
|
|
|
|
#else
|
2021-12-17 19:46:49 +00:00
|
|
|
.font(.system(size: 13))
|
2021-12-04 19:35:41 +00:00
|
|
|
.foregroundColor(.secondary)
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2021-12-05 17:14:49 +00:00
|
|
|
private var repliesButtonStackSpacing: Double {
|
|
|
|
#if os(tvOS)
|
|
|
|
24
|
|
|
|
#elseif os(iOS)
|
|
|
|
4
|
|
|
|
#else
|
|
|
|
2
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2021-12-04 19:35:41 +00:00
|
|
|
private var repliesList: some View {
|
|
|
|
Group {
|
|
|
|
let last = comments.replies.last
|
|
|
|
ForEach(comments.replies) { comment in
|
2024-08-19 23:23:54 +00:00
|
|
|
Self(comment: comment, repliesID: $repliesID, availableWidth: availableWidth - 22)
|
2021-12-04 19:35:41 +00:00
|
|
|
#if os(tvOS)
|
|
|
|
.focusable()
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if comment != last {
|
|
|
|
Divider()
|
|
|
|
.padding(.vertical, 5)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-12-05 17:14:49 +00:00
|
|
|
.padding(.leading, 22)
|
2021-12-04 19:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private var commentText: some View {
|
2023-10-15 11:35:23 +00:00
|
|
|
Group {
|
2024-08-19 22:52:04 +00:00
|
|
|
let rawText = comment.text
|
2023-10-15 11:35:23 +00:00
|
|
|
if #available(iOS 15.0, macOS 12.0, *) {
|
2024-08-19 22:52:04 +00:00
|
|
|
#if os(iOS)
|
|
|
|
ActiveLabelCommentRepresentable(
|
|
|
|
text: rawText,
|
|
|
|
availableWidth: availableWidth
|
|
|
|
)
|
|
|
|
#elseif os(macOS)
|
|
|
|
Text(rawText)
|
|
|
|
.font(.system(size: 14))
|
|
|
|
.lineSpacing(3)
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
.textSelection(.enabled)
|
2024-08-24 12:21:52 +00:00
|
|
|
#else
|
|
|
|
Text(comment.text)
|
2023-10-15 11:35:23 +00:00
|
|
|
#endif
|
|
|
|
} else {
|
2024-08-19 22:52:04 +00:00
|
|
|
Text(rawText)
|
|
|
|
.font(.system(size: 15))
|
|
|
|
.lineSpacing(3)
|
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
2023-10-15 11:35:23 +00:00
|
|
|
}
|
|
|
|
}
|
2021-12-04 19:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func openChannelAction() {
|
2022-11-24 20:36:05 +00:00
|
|
|
NavigationModel.shared.openChannel(
|
2021-12-17 19:46:49 +00:00
|
|
|
comment.channel,
|
2022-06-30 08:05:32 +00:00
|
|
|
navigationStyle: navigationStyle
|
2021-12-17 19:46:49 +00:00
|
|
|
)
|
2021-12-04 19:35:41 +00:00
|
|
|
}
|
|
|
|
}
|
2021-12-05 17:14:49 +00:00
|
|
|
|
2024-08-19 22:52:04 +00:00
|
|
|
#if os(iOS)
|
|
|
|
struct ActiveLabelCommentRepresentable: UIViewRepresentable {
|
|
|
|
var text: String
|
|
|
|
var availableWidth: CGFloat
|
|
|
|
|
|
|
|
@State private var label = ActiveLabel()
|
|
|
|
|
|
|
|
@Environment(\.openURL) private var openURL
|
|
|
|
|
|
|
|
var player = PlayerModel.shared
|
|
|
|
|
|
|
|
func makeUIView(context _: Context) -> some UIView {
|
|
|
|
customizeLabel()
|
|
|
|
return label
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateUIView(_: UIViewType, context _: Context) {
|
|
|
|
label.preferredMaxLayoutWidth = availableWidth
|
|
|
|
}
|
|
|
|
|
|
|
|
func customizeLabel() {
|
|
|
|
label.customize { label in
|
|
|
|
label.enabledTypes = [.url, .timestamp]
|
|
|
|
label.text = text
|
|
|
|
label.font = .systemFont(ofSize: 15)
|
|
|
|
label.lineSpacing = 3
|
|
|
|
label.preferredMaxLayoutWidth = availableWidth
|
|
|
|
label.URLColor = UIColor(Color.accentColor)
|
|
|
|
label.timestampColor = UIColor(Color.accentColor)
|
|
|
|
label.handleURLTap(urlTapHandler(_:))
|
|
|
|
label.handleTimestampTap(timestampTapHandler(_:))
|
|
|
|
label.numberOfLines = 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func urlTapHandler(_ url: URL) {
|
|
|
|
var urlToOpen = url
|
|
|
|
|
|
|
|
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
|
|
|
|
components.scheme = "yattee"
|
|
|
|
if let yatteeURL = components.url {
|
|
|
|
let parser = URLParser(url: urlToOpen, allowFileURLs: false)
|
|
|
|
let destination = parser.destination
|
|
|
|
if destination == .video,
|
|
|
|
parser.videoID == player.currentVideo?.videoID,
|
|
|
|
let time = parser.time
|
|
|
|
{
|
|
|
|
player.backend.seek(to: Double(time), seekType: .userInteracted)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if destination != nil {
|
|
|
|
urlToOpen = yatteeURL
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
openURL(urlToOpen)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func timestampTapHandler(_ timestamp: Timestamp) {
|
|
|
|
player.backend.seek(to: timestamp.timeInterval, seekType: .userInteracted)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2021-12-05 17:14:49 +00:00
|
|
|
struct CommentView_Previews: PreviewProvider {
|
|
|
|
static var fixture: Comment {
|
|
|
|
Comment.fixture
|
|
|
|
}
|
|
|
|
|
|
|
|
static var previews: some View {
|
2024-08-19 22:52:04 +00:00
|
|
|
CommentView(comment: fixture, repliesID: .constant(fixture.id), availableWidth: 375)
|
2021-12-17 19:46:49 +00:00
|
|
|
.padding(5)
|
2021-12-05 17:14:49 +00:00
|
|
|
}
|
|
|
|
}
|