mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
333 lines
13 KiB
Swift
333 lines
13 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SyncCore
|
|
import TelegramPresentationData
|
|
import ItemListUI
|
|
import PresentationDataUtils
|
|
import OverlayStatusController
|
|
import AccountContext
|
|
|
|
private enum CallFeedbackReason: Int32, CaseIterable {
|
|
case echo
|
|
case noise
|
|
case interruption
|
|
case distortedSpeech
|
|
case silentLocal
|
|
case silentRemote
|
|
case dropped
|
|
|
|
var hashtag: String {
|
|
switch self {
|
|
case .echo:
|
|
return "echo"
|
|
case .noise:
|
|
return "noise"
|
|
case .interruption:
|
|
return "interruptions"
|
|
case .distortedSpeech:
|
|
return "distorted_speech"
|
|
case .silentLocal:
|
|
return "silent_local"
|
|
case .silentRemote:
|
|
return "silent_remote"
|
|
case .dropped:
|
|
return "dropped"
|
|
}
|
|
}
|
|
|
|
static func localizedString(for reason: CallFeedbackReason, strings: PresentationStrings) -> String {
|
|
switch reason {
|
|
case .echo:
|
|
return strings.CallFeedback_ReasonEcho
|
|
case .noise:
|
|
return strings.CallFeedback_ReasonNoise
|
|
case .interruption:
|
|
return strings.CallFeedback_ReasonInterruption
|
|
case .distortedSpeech:
|
|
return strings.CallFeedback_ReasonDistortedSpeech
|
|
case .silentLocal:
|
|
return strings.CallFeedback_ReasonSilentLocal
|
|
case .silentRemote:
|
|
return strings.CallFeedback_ReasonSilentRemote
|
|
case .dropped:
|
|
return strings.CallFeedback_ReasonDropped
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class CallFeedbackControllerArguments {
|
|
let updateComment: (String) -> Void
|
|
let scrollToComment: () -> Void
|
|
let toggleReason: (CallFeedbackReason, Bool) -> Void
|
|
let toggleIncludeLogs: (Bool) -> Void
|
|
|
|
init(updateComment: @escaping (String) -> Void, scrollToComment: @escaping () -> Void, toggleReason: @escaping (CallFeedbackReason, Bool) -> Void, toggleIncludeLogs: @escaping (Bool) -> Void) {
|
|
self.updateComment = updateComment
|
|
self.scrollToComment = scrollToComment
|
|
self.toggleReason = toggleReason
|
|
self.toggleIncludeLogs = toggleIncludeLogs
|
|
}
|
|
}
|
|
|
|
private enum CallFeedbackControllerSection: Int32 {
|
|
case reasons
|
|
case comment
|
|
case logs
|
|
}
|
|
|
|
private enum CallFeedbackControllerEntryTag: ItemListItemTag {
|
|
case comment
|
|
|
|
func isEqual(to other: ItemListItemTag) -> Bool {
|
|
if let other = other as? CallFeedbackControllerEntryTagv {
|
|
return self == other
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum CallFeedbackControllerEntry: ItemListNodeEntry {
|
|
case reasonsHeader(PresentationTheme, String)
|
|
case reason(PresentationTheme, CallFeedbackReason, String, Bool)
|
|
case comment(PresentationTheme, String, String)
|
|
case includeLogs(PresentationTheme, String, Bool)
|
|
case includeLogsInfo(PresentationTheme, String)
|
|
|
|
var section: ItemListSectionId {
|
|
switch self {
|
|
case .reasonsHeader, .reason:
|
|
return CallFeedbackControllerSection.reasons.rawValue
|
|
case .comment:
|
|
return CallFeedbackControllerSection.comment.rawValue
|
|
case .includeLogs, .includeLogsInfo:
|
|
return CallFeedbackControllerSection.logs.rawValue
|
|
}
|
|
}
|
|
|
|
var stableId: Int32 {
|
|
switch self {
|
|
case .reasonsHeader:
|
|
return 0
|
|
case let .reason(_, reason, _, _):
|
|
return 1 + reason.rawValue
|
|
case .comment:
|
|
return 100
|
|
case .includeLogs:
|
|
return 101
|
|
case .includeLogsInfo:
|
|
return 102
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: CallFeedbackControllerEntry, rhs: CallFeedbackControllerEntry) -> Bool {
|
|
switch lhs {
|
|
case let .reasonsHeader(lhsTheme, lhsText):
|
|
if case let .reasonsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .reason(lhsTheme, lhsReason, lhsText, lhsValue):
|
|
if case let .reason(rhsTheme, rhsReason, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsReason == rhsReason, lhsText == rhsText, lhsValue == rhsValue {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .comment(lhsTheme, lhsText, lhsValue):
|
|
if case let .comment(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .includeLogs(lhsTheme, lhsText, lhsValue):
|
|
if case let .includeLogs(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .includeLogsInfo(lhsTheme, lhsText):
|
|
if case let .includeLogsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
static func <(lhs: CallFeedbackControllerEntry, rhs: CallFeedbackControllerEntry) -> Bool {
|
|
return lhs.stableId < rhs.stableId
|
|
}
|
|
|
|
func item(_ arguments: Any) -> ListViewItem {
|
|
let arguments = arguments as! CallFeedbackControllerArguments
|
|
switch self {
|
|
case let .reasonsHeader(theme, text):
|
|
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
|
case let .reason(theme, reason, title, value):
|
|
return ItemListSwitchItem(theme: theme, title: title, value: value, maximumNumberOfLines: 2, sectionId: self.section, style: .blocks, updated: { value in
|
|
arguments.toggleReason(reason, value)
|
|
})
|
|
case let .comment(theme, text, placeholder):
|
|
return ItemListMultilineInputItem(theme: theme, text: text, placeholder: placeholder, maxLength: nil, sectionId: self.section, style: .blocks, textUpdated: { updatedText in
|
|
arguments.updateComment(updatedText)
|
|
}, updatedFocus: { focused in
|
|
if focused {
|
|
arguments.scrollToComment()
|
|
}
|
|
}, tag: CallFeedbackControllerEntryTag.comment)
|
|
case let .includeLogs(theme, title, value):
|
|
return ItemListSwitchItem(theme: theme, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
|
arguments.toggleIncludeLogs(value)
|
|
})
|
|
case let .includeLogsInfo(theme, text):
|
|
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct CallFeedbackState: Equatable {
|
|
let reasons: Set<CallFeedbackReason>
|
|
let comment: String
|
|
let includeLogs: Bool
|
|
|
|
init(reasons: Set<CallFeedbackReason> = Set(), comment: String = "", includeLogs: Bool = true) {
|
|
self.reasons = reasons
|
|
self.comment = comment
|
|
self.includeLogs = includeLogs
|
|
}
|
|
|
|
func withUpdatedReasons(_ reasons: Set<CallFeedbackReason>) -> CallFeedbackState {
|
|
return CallFeedbackState(reasons: reasons, comment: self.comment, includeLogs: self.includeLogs)
|
|
}
|
|
|
|
func withUpdatedComment(_ comment: String) -> CallFeedbackState {
|
|
return CallFeedbackState(reasons: self.reasons, comment: comment, includeLogs: self.includeLogs)
|
|
}
|
|
|
|
func withUpdatedIncludeLogs(_ includeLogs: Bool) -> CallFeedbackState {
|
|
return CallFeedbackState(reasons: self.reasons, comment: self.comment, includeLogs: includeLogs)
|
|
}
|
|
}
|
|
|
|
private func callFeedbackControllerEntries(theme: PresentationTheme, strings: PresentationStrings, state: CallFeedbackState) -> [CallFeedbackControllerEntry] {
|
|
var entries: [CallFeedbackControllerEntry] = []
|
|
|
|
entries.append(.reasonsHeader(theme, strings.CallFeedback_WhatWentWrong))
|
|
for reason in CallFeedbackReason.allCases {
|
|
entries.append(.reason(theme, reason, CallFeedbackReason.localizedString(for: reason, strings: strings), state.reasons.contains(reason)))
|
|
}
|
|
|
|
entries.append(.comment(theme, state.comment, strings.CallFeedback_AddComment))
|
|
|
|
entries.append(.includeLogs(theme, strings.CallFeedback_IncludeLogs, state.includeLogs))
|
|
entries.append(.includeLogsInfo(theme, strings.CallFeedback_IncludeLogsInfo))
|
|
|
|
return entries
|
|
}
|
|
|
|
public func callFeedbackController(sharedContext: SharedAccountContext, account: Account, callId: CallId, rating: Int, userInitiated: Bool) -> ViewController {
|
|
let initialState = CallFeedbackState()
|
|
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
|
let stateValue = Atomic(value: initialState)
|
|
let updateState: ((CallFeedbackState) -> CallFeedbackState) -> Void = { f in
|
|
statePromise.set(stateValue.modify { f($0) })
|
|
}
|
|
|
|
var presentControllerImpl: ((ViewController) -> Void)?
|
|
var dismissImpl: (() -> Void)?
|
|
var ensureItemVisibleImpl: ((CallFeedbackControllerEntryTag, Bool) -> Void)?
|
|
|
|
let arguments = CallFeedbackControllerArguments(updateComment: { value in
|
|
updateState { $0.withUpdatedComment(value) }
|
|
ensureItemVisibleImpl?(.comment, false)
|
|
}, scrollToComment: {
|
|
ensureItemVisibleImpl?(.comment, true)
|
|
}, toggleReason: { reason, value in
|
|
updateState { current in
|
|
var reasons = current.reasons
|
|
if value {
|
|
reasons.insert(reason)
|
|
} else {
|
|
reasons.remove(reason)
|
|
}
|
|
return current.withUpdatedReasons(reasons)
|
|
}
|
|
}, toggleIncludeLogs: { value in
|
|
updateState { $0.withUpdatedIncludeLogs(value) }
|
|
})
|
|
|
|
let signal = combineLatest(sharedContext.presentationData, statePromise.get())
|
|
|> deliverOnMainQueue
|
|
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|
let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
|
dismissImpl?()
|
|
})
|
|
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.CallFeedback_Send), style: .bold, enabled: true, action: {
|
|
var comment = state.comment
|
|
var hashtags = ""
|
|
for reason in CallFeedbackReason.allCases {
|
|
if state.reasons.contains(reason) {
|
|
if !hashtags.isEmpty {
|
|
hashtags.append(" ")
|
|
}
|
|
hashtags.append("#\(reason.hashtag)")
|
|
}
|
|
}
|
|
if !comment.isEmpty && !state.reasons.isEmpty {
|
|
comment.append("\n")
|
|
}
|
|
comment.append(hashtags)
|
|
|
|
let _ = rateCallAndSendLogs(account: account, callId: callId, starsCount: rating, comment: comment, userInitiated: userInitiated, includeLogs: state.includeLogs).start()
|
|
dismissImpl?()
|
|
|
|
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .starSuccess(presentationData.strings.CallFeedback_Success)))
|
|
})
|
|
|
|
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.CallFeedback_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
|
let listState = ItemListNodeState(entries: callFeedbackControllerEntries(theme: presentationData.theme, strings: presentationData.strings, state: state), style: .blocks, animateChanges: false)
|
|
|
|
return (controllerState, (listState, arguments))
|
|
}
|
|
|
|
|
|
let controller = ItemListController(sharedContext: sharedContext, state: signal)
|
|
controller.navigationPresentation = .modal
|
|
presentControllerImpl = { [weak controller] c in
|
|
controller?.present(c, in: .window(.root))
|
|
}
|
|
dismissImpl = { [weak controller] in
|
|
controller?.view.endEditing(true)
|
|
controller?.dismiss()
|
|
}
|
|
ensureItemVisibleImpl = { [weak controller] targetTag, animated in
|
|
controller?.afterLayout({
|
|
guard let controller = controller else {
|
|
return
|
|
}
|
|
|
|
var resultItemNode: ListViewItemNode?
|
|
let state = stateValue.with({ $0 })
|
|
let _ = controller.frameForItemNode({ itemNode in
|
|
if let itemNode = itemNode as? ItemListItemNode {
|
|
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
|
|
resultItemNode = itemNode as? ListViewItemNode
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
|
|
if let resultItemNode = resultItemNode {
|
|
controller.ensureItemNodeVisible(resultItemNode, animated: animated)
|
|
}
|
|
})
|
|
}
|
|
return controller
|
|
}
|