GetStream / stream-chat-swiftui

SwiftUI Chat SDK ➜ Stream Chat 💬
https://getstream.io/chat/sdk/swiftui/
Other
344 stars 85 forks source link

TapGesture blocked in parts of custom Reactions Overlay View #186

Closed rpachecoaimonkey closed 2 years ago

rpachecoaimonkey commented 2 years ago

What did you do?

Created my own view for contextMenu and setting it using the factory method makeReactionsOverlayView, passing onBackgroundTap method to my custom view and using this method to dismiss the view. When it happens, the reactions are blocked too. We tested with differents messages and this scenario happens just in a few cases, and normally, in cases when the message is of the same user logged in.

There is a function or variable can we pass to block the context menu when the message is my, or we need to configure something else?

image

// Factory
    func makeReactionsOverlayView(channel: ChatChannel, currentSnapshot: UIImage, messageDisplayInfo: MessageDisplayInfo, onBackgroundTap: @escaping () -> Void, onActionExecuted: @escaping (MessageActionInfo) -> Void) -> some View {
        return CustomContextMenuView(dismissAction: onBackgroundTap, messageDisplayInfo: messageDisplayInfo)
    }

// CustomContextMenuView
struct CustomContextMenuView: View {
    @State var dismissAction: () -> Void
    @State var messageDisplayInfo: MessageDisplayInfo
    var body: some View {
        ZStack {
            VisualEffectView(effect: UIBlurEffect(style: .regular))
                .contentShape(Rectangle())
                .onTapGesture {
                    dismissAction()
                }
            VStack {
                Spacer()
                CustomReactionsView(message: messageDisplayInfo.message) {
                    dismissAction()
                }
                CustomMessageView(message: messageDisplayInfo.message, showCenter: true)
                ZStack {
                    VStack(spacing: 0) {
                        Button {
                            dismissAction()
                            // WARNING!!! this will be updated in a major release of the Stream chat SDK
                            NotificationCenter.default.post(
                                name: NSNotification.Name(MessageRepliesConstants.selectedMessageThread),
                                object: nil,
                                userInfo: [MessageRepliesConstants.selectedMessage: messageDisplayInfo.message]
                            )
                        } label: {
                            HStack(spacing: 20) {
                                Image("replay-message")
                                    .resizable()
                                    .frame(width: 18, height: 15)
                                    .foregroundColor(Color.white)
                                    .aspectRatio(contentMode: .fit)
                                Text("Reply")
                                    .font(Font.custom(FlymachineFont.NeueSingular.medium.rawValue, size: 16))
                                    .foregroundColor(Color.white)
                                Spacer()
                            }
                            .padding(13)
                        }

                        Rectangle()
                            .foregroundColor(Color("gray800").opacity(0.5))
                            .frame(height: 1)
                            .padding(.horizontal, 1)

                        Button {
                            dismissAction()
                            NotificationCenter.default.post(name: .showActionSheetToReportMessage, object: messageDisplayInfo)
                        } label: {
                            HStack(spacing: 20) {
                                Image("report-message")
                                    .resizable()
                                    .frame(width: 15, height: 17)
                                    .foregroundColor(Color("WarningDark"))
                                    .aspectRatio(contentMode: .fit)
                                Text("Report Message")
                                    .font(Font.custom(FlymachineFont.NeueSingular.medium.rawValue, size: 16))
                                    .foregroundColor(Color("WarningDark"))
                                Spacer()
                            }
                            .padding(13)
                        }

                        Rectangle()
                            .foregroundColor(Color("gray800").opacity(0.5))
                            .frame(height: 1)
                            .padding(.horizontal, 1)

                        Button {
                            dismissAction()
                            NotificationCenter.default.post(name: .showActionSheetToBlockUser, object: messageDisplayInfo)
                        } label: {
                            HStack(spacing: 20) {
                                Image("block-user")
                                    .resizable()
                                    .frame(width: 20, height: 20)
                                    .foregroundColor(Color("errorLight"))
                                    .aspectRatio(contentMode: .fit)
                                Text("Block User")
                                    .font(Font.custom(FlymachineFont.NeueSingular.medium.rawValue, size: 16))
                                    .foregroundColor(Color("errorLight"))
                                Spacer()
                            }
                            .padding(13)
                        }

                    }
                        .background(
                            RoundedRectangle(cornerRadius: 24)
                            .foregroundColor(Color("gray1000"))
                        )
                        .frame(width: 230)
                }
                .padding(1)
                .background(
                    RoundedRectangle(cornerRadius: 24)
                        .foregroundColor(Color("gray800").opacity(0.5))
                )
                Spacer()
            }
        }
        .transition(.contextMenuTransition())
    }
}

// VisualEffectView
struct VisualEffectView: UIViewRepresentable {
    var effect: UIVisualEffect?
    func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView {
        let effectView = UIVisualEffectView()
        effectView.alpha = 0.45
        effectView.backgroundColor = .gray900
        return effectView
    }
    func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) { uiView.effect = effect }
}

// CustomReactionsView
struct CustomReactionsView: View {

    @AppStorage("messageId", store: .standard) var messageId : String = ""
    @AppStorage("messageUserId", store: .standard) var messageUserId : Int = 0

    @State var message: ChatMessage
    @State private var showReactionsBG = 0
    @State private var showLOLReaction = 0
    @State private var showLikeReaction = 0
    @State private var showLoveReaction = 0
    @State private var showUnlikeReaction = 0
    @State private var rotateThumb = -45
    @State private var showWhatReaction = 0
    let inboundBubbleColor = Color(#colorLiteral(red: 0.07058823529, green: 0.07843137255, blue: 0.0862745098, alpha: 1))
    let reactionsBGColor = Color("gray800")

    var onReactionTap: () -> Void

    var body: some View {
        ZStack {

            HStack(spacing: 20) {
                CustomReactionIconView(isActive: isReactionActive("reaction-like"), iconName: "reaction-like") {
                    reaction in
                    StreamChatClient.shared.reactToMessage(message, with: reaction)
                    onReactionTap()
                }
                .scaleEffect(Double(showLoveReaction))
                CustomReactionIconView(isActive: isReactionActive("reaction-fine"), iconName: "reaction-fine") {
                    reaction in
                    StreamChatClient.shared.reactToMessage(message, with: reaction)
                    onReactionTap()
                }
                .scaleEffect(Double(showLikeReaction))
                .rotationEffect(.degrees(Double(rotateThumb)), anchor: .bottomLeading)
                CustomReactionIconView(isActive: isReactionActive("reaction-notFine"), iconName: "reaction-notFine") {
                    reaction in
                    StreamChatClient.shared.reactToMessage(message, with: reaction)
                    onReactionTap()
                }
                .scaleEffect(Double(showUnlikeReaction))
                .rotationEffect(.degrees(Double(rotateThumb)), anchor: .topTrailing)
                CustomReactionIconView(isActive: isReactionActive("reaction-lol"), iconName: "reaction-lol") {
                    reaction in
                    StreamChatClient.shared.reactToMessage(message, with: reaction)
                    onReactionTap()
                }
                .scaleEffect(Double(showLOLReaction))
                CustomReactionIconView(isActive: isReactionActive("reaction-shock"), iconName: "reaction-shock") {
                    reaction in
                    StreamChatClient.shared.reactToMessage(message, with: reaction)
                    onReactionTap()
                }
                .scaleEffect(Double(showWhatReaction))
            }
            .padding(12)

        }
        .background(
            ZStack {
                RoundedRectangle(cornerRadius: 28)
                    .foregroundColor(reactionsBGColor)
                    .scaleEffect(Double(showReactionsBG), anchor: .topTrailing)
                    .animation(.interpolatingSpring(stiffness: 170, damping: 15).delay(0.05), value: showReactionsBG)
                Circle().foregroundColor(reactionsBGColor)
                    .frame(width: 14, height: 14)
                    .offset(x: -15, y: 20)
                Circle().foregroundColor(reactionsBGColor)
                    .frame(width: 7, height: 7)
                    .offset(x: -8, y: 30)
            }
        )
        .onAppear{
                showReactionsBG = 1

                withAnimation(.interpolatingSpring(stiffness: 170, damping: 8).delay(0.1)) {
                    showLoveReaction = 1

                }

                withAnimation(.interpolatingSpring(stiffness: 170, damping: 8).delay(0.2)) {
                    showLikeReaction = 1
                    rotateThumb = 0
                }

                withAnimation(.interpolatingSpring(stiffness: 170, damping: 8).delay(0.3)) {
                    showUnlikeReaction = 1
                }

                withAnimation(.interpolatingSpring(stiffness: 170, damping: 8).delay(0.4)) {
                    showLOLReaction = 1
                    rotateThumb = 0
                }

                withAnimation(.interpolatingSpring(stiffness: 170, damping: 8).delay(0.5)) {
                    showWhatReaction = 1
                }
        }
    }

    func isReactionActive(_ reaction: String) -> Bool {
        messageId = message.id.description
        messageUserId = Int(message.author.id.description) ?? 0

        let reactions = message.currentUserReactions.map(\.type)
        return reactions.contains( .init(rawValue: reaction) )
    }
}

// CustomReactionIconView
struct CustomReactionIconView: View {
    let reactionsBGColor = Color(#colorLiteral(red: 0.07058823529, green: 0.07843137255, blue: 0.0862745098, alpha: 1))
    // Animation States
    @State private var numberOfLikes = 0
    @State private var removeInnerStroke = 14
    @State private var chromaRotate = 0
    @State private var animateTopPlus = 1
    @State private var animateMiddlePlus = 1
    @State private var animateBottomPlus = 1
    @State var isActive: Bool
    @State var iconName: String
    var animate: Bool

    var onReactionTap: (String) -> Void

    init(isActive: Bool, iconName: String, onReactionTap: @escaping (String) -> Void) {
        self.animate = !isActive
        self.isActive = isActive
        self.iconName = iconName
        self.onReactionTap = onReactionTap
    }

    var body: some View {
        ZStack{
            if !isActive {
                Image(iconName)
                    .foregroundColor(.white.opacity(0.5))
                    .frame(width: 24, height: 21)
            } else {
                Image(iconName)
                    .font(.system(size: 24))
                    .frame(width: 24, height: 21)
                    .foregroundColor(Color("primary900"))
                    .overlay(
                        ZStack {
                            Circle()
                                .strokeBorder(lineWidth: CGFloat(removeInnerStroke))
                                .frame(width: 24, height: 21)
                                .foregroundColor(Color("primary900"))
                                .hueRotation(.degrees(Double(chromaRotate)))
                            VStack {
                                Image(iconName)
                                    .scaleEffect(CGFloat(animateTopPlus))
                                    .foregroundColor(Color("primary900"))
                                Image(systemName: "plus")
                                    .scaleEffect(CGFloat(animateMiddlePlus))
                                Image(iconName)
                                    .scaleEffect(CGFloat(animateBottomPlus))
                                    .foregroundColor(Color("primary900"))
                            }
                        }.isHidden(!animate)
                    )
            }
        }
        .simultaneousGesture(TapGesture().onEnded{
            if (isActive) {
                onReactionTap(iconName)
            } else {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
                    onReactionTap(iconName)
                }
            }
            withAnimation(.easeInOut(duration: 0.25)){
                isActive.toggle()
            }

            withAnimation(.easeOut(duration: 0.5)){
                removeInnerStroke = 0
                chromaRotate = 270
            }

            withAnimation(.easeOut(duration: 0.5).delay(0.1)){
                animateTopPlus = 0
            }

            withAnimation(.easeInOut(duration: 0.5).delay(0.2)){
                animateMiddlePlus = 0
            }

            withAnimation(.spring()){
                animateBottomPlus = 0
            }
        })
    }
}

// CustomMessageView
struct CustomMessageView: View {

    @State var message: ChatMessage
    @State var showCenter: Bool = false

    var body: some View {
        HStack {
            CustomAvatarView(userInfo: message.authorDisplayInfo)
                .frame(alignment: .leading)
            CustomTextView(username: message.authorDisplayInfo.name, message: message.text)
                .frame(alignment: .leading)
            if !showCenter {
                Spacer()
            }
        }
    }
}

What did you expect to happen?

When users tapped in background, could execute the onTapGesture in the background and will tap on reactions too.

What happened instead?

onTapGesture is not called

GetStream Environment

GetStream Chat version: 4.20.0 GetStream Chat frameworks: StreamChatSwiftUI iOS version: iOS 15.6.1 Swift version: 5 Xcode version: 13.4.1 Device: iPhone 11

Additional context

martinmitrevski commented 2 years ago

Hey @rpachecoaimonkey,

Sorry, I'm not sure I understand the issue fully. By "the reactions are blocked" you mean they can't be dismissed, right?

In any case, I've tried your code and whenever I tap on the background, it correctly dismisses the popup, both for the current user's messages and for the others. (The only difference was that I was using the message text and avatar views from the SDK, since you haven't shared those).

In general, there shouldn't be any special handling for the messages sent by the current user - the overlay should behave the same. What other common thing those messages have? Maybe a custom attachment, thread replies or something else?

If possible, please share a minimum reproducible project and we can have a look. Additionally, you can also check if something is blocking the tap gesture in your custom views.

Looking forward to your input, Martin

martinmitrevski commented 2 years ago

Closing this one. If you need any additional help here, feel free to reopen it.