mirror of
https://github.com/twofas/2fas-ios.git
synced 2024-11-25 19:59:58 +01:00
Merge branch 'release/5.3.9' into feature/TF-1571
This commit is contained in:
commit
5e134a9ba6
@ -20,7 +20,7 @@
|
||||
import Foundation
|
||||
|
||||
public enum SortType: String, CaseIterable, Equatable {
|
||||
case manual
|
||||
case az
|
||||
case za
|
||||
case manual
|
||||
}
|
||||
|
@ -9,6 +9,9 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
8F594C422C3027160066562F /* AppleWatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F594C412C3027160066562F /* AppleWatchView.swift */; };
|
||||
8F594C442C3054F90066562F /* AppleWatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F594C432C3054F90066562F /* AppleWatchViewController.swift */; };
|
||||
8F00E46A2C23953B001F15AD /* SortTokensView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F00E4692C23953B001F15AD /* SortTokensView.swift */; };
|
||||
8F872DE92C24EE2600160D14 /* SortTokensPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F872DE82C24EE2600160D14 /* SortTokensPresenter.swift */; };
|
||||
8F872DEB2C24EE5500160D14 /* SortTokensInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F872DEA2C24EE5500160D14 /* SortTokensInteractor.swift */; };
|
||||
8F5C0C6C2BFFAC8A00D73ADE /* TwoFAS.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8F5C0C6A2BFFAC8900D73ADE /* TwoFAS.xcdatamodeld */; };
|
||||
8F5C0C6F2BFFACA900D73ADE /* Sync.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8F5C0C6D2BFFACA900D73ADE /* Sync.xcdatamodeld */; };
|
||||
8F7952532C01D1940053F776 /* KeyboardButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7952522C01D1940053F776 /* KeyboardButtonStyle.swift */; };
|
||||
@ -1880,6 +1883,9 @@
|
||||
/* Begin PBXFileReference section */
|
||||
8F594C412C3027160066562F /* AppleWatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchView.swift; sourceTree = "<group>"; };
|
||||
8F594C432C3054F90066562F /* AppleWatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchViewController.swift; sourceTree = "<group>"; };
|
||||
8F00E4692C23953B001F15AD /* SortTokensView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensView.swift; sourceTree = "<group>"; };
|
||||
8F872DE82C24EE2600160D14 /* SortTokensPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensPresenter.swift; sourceTree = "<group>"; };
|
||||
8F872DEA2C24EE5500160D14 /* SortTokensInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensInteractor.swift; sourceTree = "<group>"; };
|
||||
8F5C0C6B2BFFAC8900D73ADE /* TwoFAS.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TwoFAS.xcdatamodel; sourceTree = "<group>"; };
|
||||
8F5C0C6E2BFFACA900D73ADE /* Sync.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Sync.xcdatamodel; sourceTree = "<group>"; };
|
||||
8F7952522C01D1940053F776 /* KeyboardButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardButtonStyle.swift; sourceTree = "<group>"; };
|
||||
@ -3325,6 +3331,16 @@
|
||||
path = AppleWatch;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8F00E4682C239524001F15AD /* SortTokens */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8F00E4692C23953B001F15AD /* SortTokensView.swift */,
|
||||
8F872DE82C24EE2600160D14 /* SortTokensPresenter.swift */,
|
||||
8F872DEA2C24EE5500160D14 /* SortTokensInteractor.swift */,
|
||||
);
|
||||
path = SortTokens;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8F7952502C01D1770053F776 /* CommonSwiftUI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -5008,6 +5024,7 @@
|
||||
C244BE492BD07AA000F7E566 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8F00E4682C239524001F15AD /* SortTokens */,
|
||||
C244BE4B2BD07AFF00F7E566 /* Security */,
|
||||
C244BE4A2BD07AFA00F7E566 /* About */,
|
||||
C293782F2BD1A218008D5125 /* SettingsView.swift */,
|
||||
@ -10235,6 +10252,7 @@
|
||||
C2B86D2A2BC33D3000AAAC63 /* AppDelegateInteractor.swift in Sources */,
|
||||
C2A4D32E2BC0B1B20001587C /* Token.swift in Sources */,
|
||||
C268918A2BC4960600713078 /* ServiceListPresenter.swift in Sources */,
|
||||
8F00E46A2C23953B001F15AD /* SortTokensView.swift in Sources */,
|
||||
C26983822BCC8326009B3BE2 /* AppPresenter.swift in Sources */,
|
||||
C274C9D02BAB8ABB008E7212 /* TwoFASWatchApp.swift in Sources */,
|
||||
C2627F3A2BC72E96009F93A9 /* ServicePresenter.swift in Sources */,
|
||||
@ -10249,6 +10267,7 @@
|
||||
C2A4D3312BC0B2540001587C /* TokenGenerator.swift in Sources */,
|
||||
C29378382BD1BF15008D5125 /* SecurityPresenter.swift in Sources */,
|
||||
C2A4D32D2BC0B1AD0001587C /* TokenHandler.swift in Sources */,
|
||||
8F872DE92C24EE2600160D14 /* SortTokensPresenter.swift in Sources */,
|
||||
C2B86D222BC3058A00AAAC63 /* UserDefaultsRepository.swift in Sources */,
|
||||
C2B86D342BC3571E00AAAC63 /* ServiceView.swift in Sources */,
|
||||
C27D99E12BD53AB30008203F /* LogoView.swift in Sources */,
|
||||
@ -10257,6 +10276,7 @@
|
||||
C293783A2BD1BF23008D5125 /* SecurityInteractor.swift in Sources */,
|
||||
C27D99DF2BD52C8A0008203F /* WatchConsts.swift in Sources */,
|
||||
C2B86D382BC35B8300AAAC63 /* IconRenderer.swift in Sources */,
|
||||
8F872DEB2C24EE5500160D14 /* SortTokensInteractor.swift in Sources */,
|
||||
C2B86D322BC356BA00AAAC63 /* ServiceListView.swift in Sources */,
|
||||
C281A2732BD47D6C0068451C /* SuccessView.swift in Sources */,
|
||||
C268918F2BC4974800713078 /* Category.swift in Sources */,
|
||||
|
@ -1153,6 +1153,8 @@ internal enum T {
|
||||
internal static let showNextToken = T.tr("Localizable", "settings__show_next_token", fallback: "Show next token")
|
||||
/// Show next token when current one is about to expire.
|
||||
internal static let showNextTokenDesc = T.tr("Localizable", "settings__show_next_token_desc", fallback: "Show next token when current one is about to expire.")
|
||||
/// Sort tokens
|
||||
internal static let sortTokens = T.tr("Localizable", "settings__sort_tokens", fallback: "Sort tokens")
|
||||
/// An SSL error has occurred and a secure connection to the server cannot be made. Ensure you have the latest version of the app or try to change the network.
|
||||
internal static let sslErrorDescription = T.tr("Localizable", "settings__ssl_error_description", fallback: "An SSL error has occurred and a secure connection to the server cannot be made. Ensure you have the latest version of the app or try to change the network.")
|
||||
/// SSL Error
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -388,8 +388,6 @@ internal enum T {
|
||||
internal static func newServices(_ p1: Int) -> String {
|
||||
return T.tr("Localizable", "backup__new_services", p1, fallback: "%d new services")
|
||||
}
|
||||
/// This file is in a newer format version than the one the app supports
|
||||
internal static let newerFormatNotSupported = T.tr("Localizable", "backup__newer_format_not_supported", fallback: "This file is in a newer format version than the one the app supports")
|
||||
/// Nothing to import
|
||||
internal static let noNewServices = T.tr("Localizable", "backup__no_new_services", fallback: "Nothing to import")
|
||||
/// Either this file is empty, or all the services within are already available in the app
|
||||
@ -1155,6 +1153,8 @@ internal enum T {
|
||||
internal static let showNextToken = T.tr("Localizable", "settings__show_next_token", fallback: "Show next token")
|
||||
/// Show next token when current one is about to expire.
|
||||
internal static let showNextTokenDesc = T.tr("Localizable", "settings__show_next_token_desc", fallback: "Show next token when current one is about to expire.")
|
||||
/// Sort tokens
|
||||
internal static let sortTokens = T.tr("Localizable", "settings__sort_tokens", fallback: "Sort tokens")
|
||||
/// An SSL error has occurred and a secure connection to the server cannot be made. Ensure you have the latest version of the app or try to change the network.
|
||||
internal static let sslErrorDescription = T.tr("Localizable", "settings__ssl_error_description", fallback: "An SSL error has occurred and a secure connection to the server cannot be made. Ensure you have the latest version of the app or try to change the network.")
|
||||
/// SSL Error
|
||||
|
15
TwoFAS/TwoFASWatch Watch App/Assets.xcassets/NaviIconSort.imageset/Contents.json
vendored
Normal file
15
TwoFAS/TwoFASWatch Watch App/Assets.xcassets/NaviIconSort.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "NaviIconSort.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
BIN
TwoFAS/TwoFASWatch Watch App/Assets.xcassets/NaviIconSort.imageset/NaviIconSort.pdf
vendored
Normal file
BIN
TwoFAS/TwoFASWatch Watch App/Assets.xcassets/NaviIconSort.imageset/NaviIconSort.pdf
vendored
Normal file
Binary file not shown.
@ -48,4 +48,8 @@ final class InteractorFactory {
|
||||
func pinInteractor(variant: PINKeyboardVariant) -> PINKeyboardInteracting {
|
||||
PINKeyboardInteractor(mainRepository: MainRepositoryImpl.shared, variant: variant)
|
||||
}
|
||||
|
||||
func sortTokensInteractor() -> SortTokensInteracting {
|
||||
SortTokensInteractor(mainRepository: MainRepositoryImpl.shared)
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,6 @@ extension ServiceListInteractor: ServiceListInteracting {
|
||||
var hasServices: Bool { mainRepository.hasServices }
|
||||
|
||||
func listAllServices() -> [CategoryData] {
|
||||
mainRepository.listAllServicesWithingCategories(for: nil, sorting: .manual, ids: [])
|
||||
mainRepository.listAllServicesWithingCategories(for: nil, sorting: mainRepository.sortType ?? .manual, ids: [])
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,12 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum SettingsPath: Hashable {
|
||||
case security
|
||||
case about
|
||||
case sortTokens
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@Binding
|
||||
var path: NavigationPath
|
||||
@ -26,34 +32,34 @@ struct SettingsView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
NavigationLink(value: SettingsPath.security) {
|
||||
HStack {
|
||||
Image(systemName: "lock.fill")
|
||||
Text(T.Settings.security)
|
||||
.font(.callout)
|
||||
.padding(4)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
settingsRow(image: Image(systemName: "lock.fill"), title: T.Settings.security)
|
||||
}
|
||||
|
||||
|
||||
NavigationLink(value: SettingsPath.sortTokens) {
|
||||
settingsRow(image: Image(.naviIconSort), title: T.Settings.sortTokens)
|
||||
}
|
||||
|
||||
NavigationLink(value: SettingsPath.about) {
|
||||
HStack {
|
||||
Image(systemName: "info.bubble.fill")
|
||||
Text(T.Settings.about)
|
||||
.font(.callout)
|
||||
.padding(4)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
settingsRow(image: Image(systemName: "info.bubble.fill"), title: T.Settings.about)
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: SettingsPath.self) { route in
|
||||
switch route {
|
||||
case .security: SecurityView(
|
||||
path: $path,
|
||||
presenter: SecurityPresenter(
|
||||
interactor: InteractorFactory.shared.securityInteractor()
|
||||
case .security:
|
||||
SecurityView(
|
||||
path: $path,
|
||||
presenter: SecurityPresenter(
|
||||
interactor: InteractorFactory.shared.securityInteractor()
|
||||
)
|
||||
)
|
||||
case .about:
|
||||
AboutView()
|
||||
case .sortTokens:
|
||||
SortTokensView(
|
||||
presenter: SortTokensPresenter(
|
||||
interactor: InteractorFactory.shared.sortTokensInteractor()
|
||||
)
|
||||
)
|
||||
)
|
||||
case .about: AboutView()
|
||||
}
|
||||
}
|
||||
.containerBackground(.red.gradient, for: .navigation)
|
||||
@ -63,9 +69,19 @@ struct SettingsView: View {
|
||||
.navigationBarTitleDisplayMode(.automatic)
|
||||
.listItemTint(.clear)
|
||||
}
|
||||
|
||||
private func settingsRow(image: Image, title: String) -> some View {
|
||||
HStack {
|
||||
image
|
||||
.foregroundColor(.primary)
|
||||
Text(title)
|
||||
.font(.callout)
|
||||
.padding(4)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SettingsPath: Hashable {
|
||||
case security
|
||||
case about
|
||||
#Preview {
|
||||
SettingsView(path: .constant(NavigationPath()))
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
//
|
||||
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
|
||||
// Copyright © 2024 Two Factor Authentication Service, Inc.
|
||||
// Contributed by Grzegorz Machnio. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
//
|
||||
|
||||
import CommonWatch
|
||||
|
||||
protocol SortTokensInteracting: AnyObject {
|
||||
var currentSortType: SortType { get }
|
||||
|
||||
func set(_ sortType: SortType)
|
||||
}
|
||||
|
||||
final class SortTokensInteractor: SortTokensInteracting {
|
||||
var currentSortType: SortType {
|
||||
mainRepository.sortType ?? .manual
|
||||
}
|
||||
|
||||
private let mainRepository: MainRepository
|
||||
|
||||
init(mainRepository: MainRepository) {
|
||||
self.mainRepository = mainRepository
|
||||
}
|
||||
|
||||
func set(_ sortType: SortType) {
|
||||
Log("SortInteractor - saving new sort type \(sortType)", module: .interactor)
|
||||
mainRepository.setSortType(sortType)
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
//
|
||||
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
|
||||
// Copyright © 2024 Two Factor Authentication Service, Inc.
|
||||
// Contributed by Grzegorz Machnio. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
//
|
||||
|
||||
import CommonWatch
|
||||
import SwiftUI
|
||||
|
||||
protocol SortTokensPresenting: ObservableObject {
|
||||
var currentSortType: SortType { get }
|
||||
var sortTypes: [SortType] { get }
|
||||
|
||||
func onAppear()
|
||||
func set(_ sortType: SortType)
|
||||
func image(for sortType: SortType) -> Image
|
||||
}
|
||||
|
||||
final class SortTokensPresenter: SortTokensPresenting {
|
||||
@Published
|
||||
var currentSortType: SortType = .manual
|
||||
|
||||
var sortTypes: [SortType] {
|
||||
SortType.allCases
|
||||
}
|
||||
|
||||
private let interactor: SortTokensInteracting
|
||||
|
||||
init(interactor: SortTokensInteracting) {
|
||||
self.interactor = interactor
|
||||
}
|
||||
|
||||
func onAppear() {
|
||||
currentSortType = interactor.currentSortType
|
||||
}
|
||||
|
||||
func set(_ sortType: SortType) {
|
||||
interactor.set(sortType)
|
||||
currentSortType = sortType
|
||||
}
|
||||
|
||||
func image(for sortType: SortType) -> Image {
|
||||
let configuration: UIImage.SymbolConfiguration = {
|
||||
return .init(weight: currentSortType == sortType ? .heavy : .regular)
|
||||
}()
|
||||
|
||||
return Image(uiImage: sortType.image(forSelectedOption: sortType, configuration: configuration))
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
//
|
||||
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
|
||||
// Copyright © 2024 Two Factor Authentication Service, Inc.
|
||||
// Contributed by Grzegorz Machnio. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import CommonWatch
|
||||
|
||||
struct SortTokensView<Presenter: SortTokensPresenting>: View {
|
||||
@ObservedObject
|
||||
var presenter: Presenter
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(presenter.sortTypes, id: \.rawValue) { sortType in
|
||||
HStack {
|
||||
presenter.image(for: sortType)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(sortType.localized)
|
||||
.font(.callout)
|
||||
.fontWeight(presenter.currentSortType == sortType ? .heavy : .regular)
|
||||
.padding(4)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
presenter.set(sortType)
|
||||
}
|
||||
}
|
||||
}
|
||||
.containerBackground(.red.gradient, for: .navigation)
|
||||
.listStyle(.carousel)
|
||||
.environment(\.defaultMinListRowHeight, WatchConsts.minRowHeight)
|
||||
.navigationTitle(T.Settings.sortTokens)
|
||||
.navigationBarTitleDisplayMode(.automatic)
|
||||
.listItemTint(.clear)
|
||||
.onAppear {
|
||||
presenter.onAppear()
|
||||
}
|
||||
}
|
||||
|
||||
private func sortOptionRow(image: Image, title: String) -> some View {
|
||||
HStack {
|
||||
image
|
||||
.foregroundColor(.primary)
|
||||
Text(title)
|
||||
.font(.callout)
|
||||
.padding(4)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SortType {
|
||||
var localized: String {
|
||||
switch self {
|
||||
case .az: return T.Tokens.sortByAToZ
|
||||
case .za: return T.Tokens.sortByZToA
|
||||
case .manual: return T.Tokens.sortByManual
|
||||
}
|
||||
}
|
||||
|
||||
func image(forSelectedOption option: Self, configuration: UIImage.SymbolConfiguration) -> UIImage {
|
||||
var image: UIImage?
|
||||
switch self {
|
||||
case .az: image = UIImage(systemName: "arrow.down")
|
||||
case .za: image = UIImage(systemName: "arrow.up")
|
||||
case .manual: image = UIImage(systemName: "line.3.horizontal")
|
||||
}
|
||||
|
||||
if self == option {
|
||||
image = image?.withConfiguration(configuration)
|
||||
}
|
||||
return image?.withRenderingMode(.alwaysTemplate) ?? UIImage()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
final class SortTokensPresenterMock: SortTokensPresenting {
|
||||
var currentSortType: CommonWatch.SortType = .manual
|
||||
var sortTypes: [CommonWatch.SortType] = CommonWatch.SortType.allCases
|
||||
|
||||
func onAppear() {}
|
||||
func set(_ sortType: SortType) {}
|
||||
func image(for sortType: SortType) -> Image {
|
||||
.init(systemName: "arrowDown")
|
||||
}
|
||||
}
|
||||
|
||||
return SortTokensView(presenter: SortTokensPresenterMock())
|
||||
}
|
Loading…
Reference in New Issue
Block a user