Rageshake Service (#70)

This commit is contained in:
ismailgulek
2022-06-06 12:38:07 +03:00
committed by GitHub
parent 1e7243fcf9
commit fa0721b160
57 changed files with 3010 additions and 32 deletions
@@ -0,0 +1,109 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct BugReportCoordinatorParameters {
let bugReportService: BugReportServiceProtocol
let screenshot: UIImage?
}
final class BugReportCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: BugReportCoordinatorParameters
private let bugReportHostingController: UIViewController
private var bugReportViewModel: BugReportViewModelProtocol
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator?
private var errorIndicator: UserIndicator?
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: (() -> Void)?
// MARK: - Setup
init(parameters: BugReportCoordinatorParameters) {
self.parameters = parameters
let viewModel = BugReportViewModel(bugReportService: parameters.bugReportService,
screenshot: parameters.screenshot)
let view = BugReport(context: viewModel.context)
bugReportViewModel = viewModel
bugReportHostingController = UIHostingController(rootView: view)
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: bugReportHostingController)
}
// MARK: - Public
func start() {
MXLog.debug("[BugReportCoordinator] did start.")
bugReportViewModel.callback = { [weak self] result in
guard let self = self else { return }
MXLog.debug("[BugReportCoordinator] BugReportViewModel did complete with result: \(result).")
switch result {
case .submitStarted:
self.startLoading()
case .submitFinished:
self.stopLoading()
self.showSuccess(label: ElementL10n.done)
case .submitFailed(let error):
self.stopLoading()
self.showError(label: error.localizedDescription)
case .cancel:
self.completion?()
}
}
}
func toPresentable() -> UIViewController {
bugReportHostingController
}
// MARK: - Private
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
loadingIndicator = indicatorPresenter.present(.loading(label: label,
isInteractionBlocking: isInteractionBlocking))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
}
/// Show success indicator
private func showSuccess(label: String) {
errorIndicator = indicatorPresenter.present(.success(label: label))
}
/// Show error indicator
private func showError(label: String) {
errorIndicator = indicatorPresenter.present(.error(label: label))
}
}
@@ -0,0 +1,48 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import UIKit
// MARK: - Coordinator
// MARK: View model
enum BugReportViewModelAction {
case submitStarted
case submitFinished
case submitFailed(error: Error)
case cancel
}
// MARK: View
struct BugReportViewState: BindableState {
var screenshot: UIImage?
var bindings: BugReportViewStateBindings
}
struct BugReportViewStateBindings {
var reportText: String
var sendingLogsEnabled: Bool
}
enum BugReportViewAction {
case submit
case cancel
case toggleSendLogs
case removeScreenshot
}
@@ -0,0 +1,87 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14, *)
typealias BugReportViewModelType = StateStoreViewModel<BugReportViewState,
BugReportViewAction>
@available(iOS 14, *)
class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol {
// MARK: - Properties
let bugReportService: BugReportServiceProtocol
// MARK: Private
func submitBugReport() async {
callback?(.submitStarted)
do {
var files: [URL] = []
if let screenshot = state.screenshot {
let anonymized = try await ImageAnonymizer.anonymizedImage(from: screenshot)
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("screenshot").appendingPathExtension("png")
// remove old screenshot if exists
if FileManager.default.fileExists(atPath: tmpUrl.path) {
try FileManager.default.removeItem(at: tmpUrl)
}
try anonymized.dataForPNGRepresentation().write(to: tmpUrl)
files.append(tmpUrl)
}
let result = try await bugReportService.submitBugReport(text: context.reportText,
includeLogs: context.sendingLogsEnabled,
includeCrashLog: true,
githubLabels: [],
files: files)
MXLog.info("[BugReportViewModel] submitBugReport succeeded, result: \(result.reportUrl)")
callback?(.submitFinished)
} catch let error {
MXLog.error("[BugReportViewModel] submitBugReport failed: \(error)")
callback?(.submitFailed(error: error))
}
}
// MARK: Public
var callback: ((BugReportViewModelAction) -> Void)?
// MARK: - Setup
init(bugReportService: BugReportServiceProtocol,
screenshot: UIImage?) {
self.bugReportService = bugReportService
let bindings = BugReportViewStateBindings(reportText: "", sendingLogsEnabled: true)
super.init(initialViewState: BugReportViewState(screenshot: screenshot,
bindings: bindings))
}
// MARK: - Public
override func process(viewAction: BugReportViewAction) async {
switch viewAction {
case .submit:
await submitBugReport()
case .cancel:
callback?(.cancel)
case .toggleSendLogs:
context.sendingLogsEnabled.toggle()
case .removeScreenshot:
state.screenshot = nil
}
}
}
@@ -0,0 +1,25 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@MainActor
protocol BugReportViewModelProtocol {
var callback: ((BugReportViewModelAction) -> Void)? { get set }
@available(iOS 14, *)
var context: BugReportViewModelType.Context { get }
}
@@ -0,0 +1,140 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct BugReport: View {
// MARK: Private
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
private var horizontalPadding: CGFloat {
horizontalSizeClass == .regular ? 50 : 16
}
// MARK: Public
@ObservedObject var context: BugReportViewModel.Context
// MARK: Views
var body: some View {
GeometryReader { geometry in
VStack {
ScrollView {
mainContent
.padding(.top, 50)
.padding(.horizontal, horizontalPadding)
}
.introspectScrollView { scrollView in
scrollView.keyboardDismissMode = .onDrag
}
buttons
.padding(.horizontal, horizontalPadding)
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
}
.navigationTitle(ElementL10n.titleActivityBugReport)
}
}
/// The main content of the view to be shown in a scroll view.
var mainContent: some View {
VStack(alignment: .leading, spacing: 12) {
Text(ElementL10n.sendBugReportDescription)
.accessibilityIdentifier("reportBugDescription")
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
if context.reportText.isEmpty {
Text(ElementL10n.sendBugReportPlaceholder)
.foregroundColor(Color(UIColor.placeholderText))
.padding(.horizontal, 8)
.padding(.vertical, 12)
}
TextEditor(text: $context.reportText)
.padding(4)
.background(Color.clear)
.cornerRadius(8)
.accessibilityIdentifier("reportTextView")
.introspectTextView { textView in
textView.backgroundColor = .clear
}
}
.frame(maxWidth: .infinity)
.frame(height: 300)
.font(.body)
Text(ElementL10n.sendBugReportLogsDescription)
.accessibilityIdentifier("sendLogsDescription")
HStack(spacing: 12) {
Toggle(ElementL10n.sendBugReportIncludeLogs, isOn: $context.sendingLogsEnabled)
.toggleStyle(ElementToggleStyle())
.accessibilityIdentifier("sendLogsToggle")
Text(ElementL10n.sendBugReportIncludeLogs).accessibilityIdentifier("sendLogsText")
}
.onTapGesture {
context.send(viewAction: .toggleSendLogs)
}
screenshot
}
}
/// The action buttons shown at the bottom of the view.
var buttons: some View {
VStack {
Button { context.send(viewAction: .submit) } label: {
Text(ElementL10n.actionSend)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.disabled(context.reportText.count < 5)
.accessibilityIdentifier("sendButton")
}
}
@ViewBuilder
var screenshot: some View {
if let screenshot = context.viewState.screenshot {
ZStack(alignment: .topTrailing) {
Image(uiImage: screenshot)
.resizable()
.scaledToFit()
.frame(width: 100)
.accessibilityIdentifier("screenshotImage")
Button { context.send(viewAction: .removeScreenshot) } label: {
Image(uiImage: Asset.Images.closeCircle.image)
}
.offset(x: 10, y: -10)
.accessibilityIdentifier("removeScreenshotButton")
}
.padding(.vertical, 10)
}
}
}
// MARK: - Previews
struct BugReport_Previews: PreviewProvider {
static var previews: some View {
Group {
let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image)
BugReport(context: viewModel.context)
.previewInterfaceOrientation(.portrait)
}
}
}