mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-16 06:28:27 +02:00
Merge branch 'feature/7564_migration_part_1_banner' into 'develop'
feat: add migration part 1 banner (MESSENGER-7564) See merge request bwmessenger/bundesmessenger/bundesmessenger-ios!437
This commit is contained in:
@@ -38,6 +38,8 @@ class BWIBuildSettings: NSObject {
|
||||
static let additionalBwiHeaderKey = "bwiHeader"
|
||||
static let isWellknownFederationEnabled = "isWellknownFederationEnabled"
|
||||
static let isLabsFederationEnabled = "isLabsFederationEnabled"
|
||||
static let BuMXMigrationInfoLevelKey = "BuMXMigrationInfoLevel"
|
||||
static let didBuMXMigrationInfoLevelKey = "didBuMXMigrationInfoLevel"
|
||||
}
|
||||
|
||||
private struct BwiSettingsConstants {
|
||||
@@ -488,7 +490,7 @@ class BWIBuildSettings: NSObject {
|
||||
var forgotPasswordInformationAlert = true
|
||||
|
||||
// MARK: Promote new feature within a banner below the navigation view
|
||||
var showTopBanner = false
|
||||
var showTopBanner = true
|
||||
|
||||
var showCustomServerDisplayName = true
|
||||
var customServerDisplayName = ""
|
||||
@@ -674,6 +676,15 @@ class BWIBuildSettings: NSObject {
|
||||
@UserDefault(key: UserDefaultsKeys.isLabsFederationEnabled, defaultValue: false, storage: RiotSettings.defaults)
|
||||
var isLabsFederationEnabled
|
||||
|
||||
// Migration status from wellknown config
|
||||
@UserDefault(key: UserDefaultsKeys.BuMXMigrationInfoLevelKey, defaultValue: 0, storage: RiotSettings.defaults)
|
||||
var BuMXMigrationInfoLevel
|
||||
|
||||
// Migration level already shown
|
||||
@UserDefault(key: UserDefaultsKeys.didBuMXMigrationInfoLevelKey, defaultValue: 0, storage: RiotSettings.defaults)
|
||||
var didShowBuMXMigrationInfoLevel
|
||||
|
||||
|
||||
// shows the grey/green/red shield for the room avatar / user avatar
|
||||
var showEncryptionStatusBadgeOnAvatar = false
|
||||
|
||||
|
||||
9
Riot/Assets/Images.xcassets/BWI/migration_detail_1.imageset/Apple iPhone.svg
vendored
Normal file
9
Riot/Assets/Images.xcassets/BWI/migration_detail_1.imageset/Apple iPhone.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.8 MiB |
15
Riot/Assets/Images.xcassets/BWI/migration_detail_1.imageset/Contents.json
vendored
Normal file
15
Riot/Assets/Images.xcassets/BWI/migration_detail_1.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Apple iPhone.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
BIN
Riot/Assets/SharedImages.xcassets/bumx_logo.imageset/BuM_Appicon-next_iOS_1024.png
vendored
Normal file
BIN
Riot/Assets/SharedImages.xcassets/bumx_logo.imageset/BuM_Appicon-next_iOS_1024.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
15
Riot/Assets/SharedImages.xcassets/bumx_logo.imageset/Contents.json
vendored
Normal file
15
Riot/Assets/SharedImages.xcassets/bumx_logo.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "BuM_Appicon-next_iOS_1024.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
@@ -747,6 +747,35 @@
|
||||
|
||||
"room_error_join_failed_federation_disabled_message" = "Du kannst den Raum nicht beitreten. Grund: Die Föderation wurde für diesen Raum aufgehoben.";
|
||||
|
||||
// MARK: migration to new client
|
||||
// Banner
|
||||
"bwi_mobile_dialog_m_banner_title" = "Wechsel zum BundesMessengerX";
|
||||
"bwi_mobileMdialog_m1_banner_text" = "Das ist eine neue, technisch optimierte App vom BuM. Du kannst sie jetzt schon ausprobieren, bevor bald alle umsteigen müssen.";
|
||||
"bwi_mobileMdialog_m1_banner_text_bold" = " Alle deine Kontakte und Nachrichten bleiben erhalten.";
|
||||
"bwi_mobile_dialog_m_banner_button2" = "Erfahre mehr";
|
||||
"bwi_mobile_dialog_m_banner_button1" = "Später erinnern";
|
||||
|
||||
// Sheet page 1
|
||||
"bwi_mobile_dialog_m_more_title" = "BundesMessengerX";
|
||||
|
||||
"bwi_mobile_dialog_m1_more_text_1" = "Diese App wird bald abgeschaltet und durch eine neue technisch optimierte App ersetzt ";
|
||||
"bwi_mobile_dialog_m1_more_text_2" = "(alle deine Kontakte und Nachrichten bleiben erhalten).";
|
||||
"bwi_mobile_dialog_m1_more_text_3" = "\n\nDich erwartet u.a.:";
|
||||
"bwi_mobile_dialog_m1_more_text_bullet_1" = "schnellere Performance";
|
||||
"bwi_mobile_dialog_m1_more_text_bullet_2" = "verbessertes Design";
|
||||
"bwi_mobile_dialog_m1_more_text_bullet_3" = "neue Funktionen, wie Nachrichten anheften";
|
||||
"bwi_mobile_dialog_m1_more_text_bullet_4" = "optimierte Barrierefreiheit";
|
||||
|
||||
// Sheet page 2
|
||||
"bwi_mobile_dialog_m_more2_title" = "Wechsel jetzt:";
|
||||
"bwi_mobile_dialog_m_more2_text_1" = "Benutzername hier kopieren";
|
||||
"bwi_mobile_dialog_m_more2_text_2" = "App über den Button herunterladen";
|
||||
"bwi_mobile_dialog_m_more2_text_3" = "Mit deinen Anmeldedaten einloggen";
|
||||
"bwi_mobile_dialog_m_more_button" = "Neue App herunterladen";
|
||||
"bwi_mobile_dialog_m_more_success" = "Benutzername kopiert";
|
||||
|
||||
"bwi_a11y_close_button" = "Ansicht schließen";
|
||||
|
||||
// MARK owner handling
|
||||
"room_member_power_level_owner_in" = "Inhaber von %@";
|
||||
"room_member_power_level_short_owner" = "Inhaber";
|
||||
|
||||
@@ -660,6 +660,34 @@
|
||||
|
||||
"room_error_join_failed_federation_disabled_message" = "You cannot join the room. The federation for this room has been withdrawn.";
|
||||
|
||||
// MARK: migration to new client
|
||||
// Banner
|
||||
"bwi_mobile_dialog_m_banner_title" = "Change to BundesMessengerX";
|
||||
"bwi_mobileMdialog_m1_banner_text" = "This is a new technically optimized app from BuM. You can try it out now before everyone has to switch soon.";
|
||||
"bwi_mobileMdialog_m1_banner_text_bold" = " All your contacts and messages will be retained.";
|
||||
"bwi_mobile_dialog_m_banner_button2" = "Learn more";
|
||||
"bwi_mobile_dialog_m_banner_button1" = "Remember later";
|
||||
|
||||
// Sheet page 1
|
||||
"bwi_mobile_dialog_m_more_title" = "BundesMessengerX";
|
||||
"bwi_mobile_dialog_m1_more_text_1" = "This app will soon be shut down and replaced by a new technically optimized app ";
|
||||
"bwi_mobile_dialog_m1_more_text_2" = "(all your contacts and messages will remain).";
|
||||
"bwi_mobile_dialog_m1_more_text_3" = "\n\nAmong other things, you can expect:";
|
||||
"bwi_mobile_dialog_m1_more_text_bullet_1" = "faster performance";
|
||||
"bwi_mobile_dialog_m1_more_text_bullet_2" = "improved design";
|
||||
"bwi_mobile_dialog_m1_more_text_bullet_3" = "new functions, such as pinning messages";
|
||||
"bwi_mobile_dialog_m1_more_text_bullet_4" = "optimized accessibility";
|
||||
|
||||
// Sheet page 2
|
||||
"bwi_mobile_dialog_m_more2_title" = "Change now:";
|
||||
"bwi_mobile_dialog_m_more2_text_1" = "Copy user name here";
|
||||
"bwi_mobile_dialog_m_more2_text_2" = "Download app via the button";
|
||||
"bwi_mobile_dialog_m_more2_text_3" = "Log in with your credentials";
|
||||
"bwi_mobile_dialog_m_more_button" = "Download new app";
|
||||
"bwi_mobile_dialog_m_more_success" = "user name copied";
|
||||
|
||||
"bwi_a11y_close_button" = "Close window";
|
||||
|
||||
// MARK owner handling
|
||||
"room_member_power_level_owner_in" = "Owner in %@";
|
||||
"room_member_power_level_short_owner" = "Owner";
|
||||
|
||||
@@ -131,6 +131,10 @@ public class BWIL10n: NSObject {
|
||||
public static var bumAutheticationTitle: String {
|
||||
return BWIL10n.tr("Bwi", "bum_authetication_title")
|
||||
}
|
||||
/// Ansicht schließen
|
||||
public static var bwiA11yCloseButton: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_a11y_close_button")
|
||||
}
|
||||
/// Barrierefreiheitserklärung
|
||||
public static var bwiAccessibilityDeclarationButtonTitle: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_accessibility_declaration_button_title")
|
||||
@@ -299,6 +303,82 @@ public class BWIL10n: NSObject {
|
||||
public static var bwiMdmLogoutMessage: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mdm_logout_message")
|
||||
}
|
||||
/// Diese App wird bald abgeschaltet und durch eine neue technisch optimierte App ersetzt
|
||||
public static var bwiMobileDialogM1MoreText1: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m1_more_text_1")
|
||||
}
|
||||
/// (alle deine Kontakte und Nachrichten bleiben erhalten).
|
||||
public static var bwiMobileDialogM1MoreText2: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m1_more_text_2")
|
||||
}
|
||||
/// \n\nDich erwartet u.a.:
|
||||
public static var bwiMobileDialogM1MoreText3: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m1_more_text_3")
|
||||
}
|
||||
/// schnellere Performance
|
||||
public static var bwiMobileDialogM1MoreTextBullet1: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m1_more_text_bullet_1")
|
||||
}
|
||||
/// verbessertes Design
|
||||
public static var bwiMobileDialogM1MoreTextBullet2: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m1_more_text_bullet_2")
|
||||
}
|
||||
/// neue Funktionen, wie Nachrichten anheften
|
||||
public static var bwiMobileDialogM1MoreTextBullet3: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m1_more_text_bullet_3")
|
||||
}
|
||||
/// optimierte Barrierefreiheit
|
||||
public static var bwiMobileDialogM1MoreTextBullet4: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m1_more_text_bullet_4")
|
||||
}
|
||||
/// Später erinnern
|
||||
public static var bwiMobileDialogMBannerButton1: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_banner_button1")
|
||||
}
|
||||
/// Erfahre mehr
|
||||
public static var bwiMobileDialogMBannerButton2: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_banner_button2")
|
||||
}
|
||||
/// Wechsel zum BundesMessengerX
|
||||
public static var bwiMobileDialogMBannerTitle: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_banner_title")
|
||||
}
|
||||
/// Benutzername hier kopieren
|
||||
public static var bwiMobileDialogMMore2Text1: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_more2_text_1")
|
||||
}
|
||||
/// App über den Button herunterladen
|
||||
public static var bwiMobileDialogMMore2Text2: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_more2_text_2")
|
||||
}
|
||||
/// Mit deinen Anmeldedaten einloggen
|
||||
public static var bwiMobileDialogMMore2Text3: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_more2_text_3")
|
||||
}
|
||||
/// Wechsel jetzt:
|
||||
public static var bwiMobileDialogMMore2Title: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_more2_title")
|
||||
}
|
||||
/// Neue App herunterladen
|
||||
public static var bwiMobileDialogMMoreButton: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_more_button")
|
||||
}
|
||||
/// Benutzername kopiert
|
||||
public static var bwiMobileDialogMMoreSuccess: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_more_success")
|
||||
}
|
||||
/// BundesMessengerX
|
||||
public static var bwiMobileDialogMMoreTitle: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobile_dialog_m_more_title")
|
||||
}
|
||||
/// Das ist eine neue, technisch optimierte App vom BuM. Du kannst sie jetzt schon ausprobieren, bevor bald alle umsteigen müssen.
|
||||
public static var bwiMobileMdialogM1BannerText: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobileMdialog_m1_banner_text")
|
||||
}
|
||||
/// Alle deine Kontakte und Nachrichten bleiben erhalten.
|
||||
public static var bwiMobileMdialogM1BannerTextBold: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_mobileMdialog_m1_banner_text_bold")
|
||||
}
|
||||
/// Meine Notizen
|
||||
public static var bwiNotesRoomTitle: String {
|
||||
return BWIL10n.tr("Bwi", "bwi_notes_room_title")
|
||||
|
||||
@@ -63,6 +63,7 @@ internal class Asset: NSObject {
|
||||
internal static let introduceFederation2Light = ImageAsset(name: "introduce_federation_2_light")
|
||||
internal static let introduceFederation3 = ImageAsset(name: "introduce_federation_3")
|
||||
internal static let mediaFileUnavailable = ImageAsset(name: "media_file_unavailable")
|
||||
internal static let migrationDetail1 = ImageAsset(name: "migration_detail_1")
|
||||
internal static let newFeatures = ImageAsset(name: "new_features")
|
||||
internal static let qr = ImageAsset(name: "qr")
|
||||
internal static let qrcodeViewfinder = ImageAsset(name: "qrcode_viewfinder")
|
||||
@@ -411,6 +412,7 @@ internal class Asset: NSObject {
|
||||
@objc(AssetSharedImages) internal class SharedImages: NSObject {
|
||||
internal static let cancel = ImageAsset(name: "cancel")
|
||||
internal static let e2eVerified = ImageAsset(name: "e2e_verified")
|
||||
internal static let bumxLogo = ImageAsset(name: "bumx_logo")
|
||||
internal static let horizontalLogo = ImageAsset(name: "horizontal_logo")
|
||||
internal static let loginFlowLogo = ImageAsset(name: "login_flow_logo")
|
||||
internal static let radioButtonDefault = ImageAsset(name: "radio-button-default")
|
||||
|
||||
@@ -202,7 +202,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
|
||||
{
|
||||
NSMutableArray *types = [NSMutableArray array];
|
||||
// bwi: add new feature banner
|
||||
if (!shouldHideFeatureBanner && BWIBuildSettings.shared.showTopBanner && BWIBuildSettings.shared.isWellknownFederationEnabled){
|
||||
if (!shouldHideFeatureBanner && BWIBuildSettings.shared.showTopBanner && BWIBuildSettings.shared.BuMXMigrationInfoLevel > 0) {
|
||||
[types addObject:@(RecentsDataSourceSectionTypeFeatureBanner)];
|
||||
}
|
||||
|
||||
@@ -1890,17 +1890,18 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
|
||||
|
||||
- (void) shouldShowFeatureBanner
|
||||
{
|
||||
if (BWIBuildSettings.shared.showTopBanner && BWIBuildSettings.shared.isWellknownFederationEnabled){
|
||||
if (BWIBuildSettings.shared.showTopBanner && BWIBuildSettings.shared.BuMXMigrationInfoLevel > 0){
|
||||
MXSession* session = [self mxSession];
|
||||
if(!session)
|
||||
return;
|
||||
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
|
||||
FeatureBannerVisibilityService *featureBannerService = [[FeatureBannerVisibilityService alloc] initWithMxSession:session];
|
||||
[featureBannerService isUnreadWithVersion:version completion:^(BOOL unread) {
|
||||
FeatureBannerVisibilityService *featureBannerService = [FeatureBannerVisibilityService alloc];
|
||||
[featureBannerService isUnreadWithCompletion: ^(BOOL unread) {
|
||||
if (unread) {
|
||||
// this notification will be called either if the user clicked on the banner or wants to hide it
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:@"de.bwi.messenger.hide_top_banner" object:NULL queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
|
||||
[self markFeatureBannerAsRead];
|
||||
self->shouldHideFeatureBanner = TRUE;
|
||||
[self.delegate dataSource:self didCellChange:nil];
|
||||
//[self markFeatureBannerAsRead];
|
||||
}];
|
||||
|
||||
// this notification will be called either if the user clicked on the banner or wants to hide it using a swipe gesture
|
||||
@@ -1931,9 +1932,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
|
||||
MXSession* session = [self mxSession];
|
||||
if(!session)
|
||||
return;
|
||||
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
|
||||
FeatureBannerVisibilityService *featureBannerService = [[FeatureBannerVisibilityService alloc] initWithMxSession:session];
|
||||
[featureBannerService markAsReadWithVersion:version];
|
||||
FeatureBannerVisibilityService *featureBannerService = [FeatureBannerVisibilityService alloc];
|
||||
[featureBannerService markAsRead];
|
||||
self->shouldHideFeatureBanner = TRUE;
|
||||
[self.delegate dataSource:self didCellChange:nil];
|
||||
}
|
||||
|
||||
@@ -772,6 +772,11 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
|
||||
self.currentMatrixSession?.refreshHomeserverWellknown(false, success: { wellknown in
|
||||
// bwi: #5706 fix crash: only show matomo alert when wellknown is available
|
||||
wellknown?.updateFederationStatus()
|
||||
// BWI #7564 add migration level
|
||||
if let migrationLevel = wellknown?.migrationInfoLevel() {
|
||||
BWIBuildSettings.shared.BuMXMigrationInfoLevel = migrationLevel
|
||||
}
|
||||
// BWI #7564 END
|
||||
self.bwiCheckForMatomoPromt()
|
||||
}, failure: nil)
|
||||
}
|
||||
|
||||
@@ -621,10 +621,15 @@ class AllChatsViewController: HomeViewController {
|
||||
}
|
||||
// bwi: feature banner cell
|
||||
if sectionType == .featureBanner {
|
||||
var username = ""
|
||||
if let mainSession = self.mainSession {
|
||||
username = mainSession.myUser.displayname
|
||||
}
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "featureBanner", for: indexPath) as? FeatureBannerViewCell<FeatureBannerView> else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
cell.selectionStyle = .none
|
||||
cell.setUsername(username: username)
|
||||
cell.setupView(parent: self, rootView: FeatureBannerView(delegate: cell))
|
||||
featureBannerViewHeight = cell.calculateHeight()
|
||||
return cell
|
||||
|
||||
@@ -250,7 +250,11 @@ class AuthenticationService: NSObject {
|
||||
if let wellKnown = try? await wellKnown(for: homeserverURL) {
|
||||
self.wellknown = wellKnown
|
||||
wellknown?.updateFederationStatus()
|
||||
|
||||
// BWI #7564 add migration level
|
||||
if let migrationLevel = wellknown?.migrationInfoLevel() {
|
||||
BWIBuildSettings.shared.BuMXMigrationInfoLevel = migrationLevel
|
||||
}
|
||||
// BWI #7564 END
|
||||
if let baseURL = URL(string: wellKnown.homeServer.baseUrl) {
|
||||
homeserverURL = baseURL
|
||||
}
|
||||
|
||||
@@ -229,15 +229,11 @@ fileprivate func unrestrictUser(mxSession: MXSession?) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
fileprivate func unmarkBannerVersion(mxSession: MXSession?) -> Bool {
|
||||
guard let mxSession = mxSession else {
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate func unmarkBannerVersion(mxSession: MXSession?) -> Bool {
|
||||
// NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
|
||||
|
||||
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
|
||||
_ = FeatureBannerVisibilityService(mxSession: mxSession).markAsUnread(version: version)
|
||||
FeatureBannerVisibilityService().markAsUnread(version: version)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -28,6 +28,7 @@ protocol FeatureBannerDelegate {
|
||||
private var parentViewController: UIViewController?
|
||||
private let hostingController = UIHostingController<Content?>(rootView: nil)
|
||||
private var webViewController: WebViewViewController?
|
||||
private var username: String = ""
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
@@ -90,62 +91,69 @@ protocol FeatureBannerDelegate {
|
||||
}
|
||||
|
||||
func didPressShowDetails() {
|
||||
let htmlFile = BWIBuildSettings.shared.bwiFeatureHistoryFilePath
|
||||
self.webViewController = WebViewViewController(localHTMLFile: htmlFile)
|
||||
webViewController?.title = BWIL10n.bwiSettingsNewFeaturesHeader
|
||||
let navigationBar: UINavigationController = UINavigationController(rootViewController: webViewController!)
|
||||
webViewController?.navigationItem.setLeftBarButton(UIBarButtonItem(title: VectorL10n.close, style: .plain, target: self, action: #selector(closeModal)), animated: false)
|
||||
hostingController.parent?.present(navigationBar, animated: true, completion: { () -> Void in
|
||||
self.hideTopBanner()
|
||||
})
|
||||
let migrationInfoView = MigrationInfoView(username: username).environmentObject(BWIThemeService.shared).interactiveDismissDisabled(true)
|
||||
let hostingViewController = UIHostingController(rootView: migrationInfoView)
|
||||
if hostingViewController.popoverPresentationController != nil {
|
||||
hostingViewController.modalPresentationStyle = .popover
|
||||
}
|
||||
hostingController.parent?.present(hostingViewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func closeModal() {
|
||||
webViewController?.dismiss(animated: true)
|
||||
}
|
||||
|
||||
func setUsername(username: String) {
|
||||
self.username = username
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct FeatureBannerView: View {
|
||||
var delegate: FeatureBannerDelegate?
|
||||
let darkmodeBackground = UIColor(rgb:0x2394A7)
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
closeButton
|
||||
header
|
||||
advertisementText
|
||||
|
||||
if !BWIL10n.bwiFeatureBannerShowMoreButton.isEmpty {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
remindMeLaterButton
|
||||
showMoreButton
|
||||
} else {
|
||||
Spacer()
|
||||
.frame(height: 25)
|
||||
}
|
||||
}
|
||||
.background(Color(ThemeService.shared().theme.tintColor))
|
||||
.padding(.horizontal, 12)
|
||||
.background(ThemeService.shared().isCurrentThemeDark() ? Color(uiColor: darkmodeBackground) : Color(ThemeService.shared().theme.tintColor))
|
||||
.cornerRadius(12)
|
||||
.padding(16)
|
||||
}
|
||||
|
||||
var header: some View {
|
||||
HStack() {
|
||||
// Image(Asset.Images.newFeatures.name)
|
||||
Image(Asset.Images.roomFederatedBumIconDark.name)
|
||||
Image(Asset.Images.newFeatures.name)
|
||||
// .renderingMode(.template)
|
||||
// .foregroundColor(Color(ThemeService.shared().theme.backgroundColor))
|
||||
Text(BWIL10n.bwiFeatureBannerHeader)
|
||||
.foregroundColor(Color(ThemeService.shared().theme.backgroundColor))
|
||||
.accessibilityHidden(true)
|
||||
Text(BWIL10n.bwiMobileDialogMBannerTitle)
|
||||
.font(.system(size: 20).bold())
|
||||
.foregroundColor(Color(ThemeService.shared().theme.backgroundColor))
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 16)
|
||||
}
|
||||
|
||||
var advertisementText: some View {
|
||||
Text(BWIL10n.bwiFeatureBannerAdvertisementText)
|
||||
.font(.system(size: 15))
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(nil)
|
||||
.foregroundColor(Color(ThemeService.shared().theme.backgroundColor))
|
||||
.padding(5)
|
||||
VStack {
|
||||
Text(BWIL10n.bwiMobileMdialogM1BannerText) +
|
||||
Text(BWIL10n.bwiMobileMdialogM1BannerTextBold)
|
||||
.bold()
|
||||
}
|
||||
.font(.system(size: 15))
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(nil)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(Color(ThemeService.shared().theme.backgroundColor))
|
||||
}
|
||||
|
||||
var closeButton: some View {
|
||||
@@ -166,13 +174,37 @@ struct FeatureBannerView: View {
|
||||
Button {
|
||||
delegate?.didPressShowDetails()
|
||||
} label: {
|
||||
Text(BWIL10n.bwiFeatureBannerShowMoreButton)
|
||||
.font(.system(size: 15))
|
||||
.padding(10)
|
||||
.foregroundColor(Color(ThemeService.shared().theme.backgroundColor))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color(ThemeService.shared().theme.backgroundColor), lineWidth: 2))
|
||||
HStack() {
|
||||
Spacer()
|
||||
Text(BWIL10n.bwiMobileDialogMBannerButton2)
|
||||
.font(.system(size: 18))
|
||||
.bold()
|
||||
.padding(10)
|
||||
.foregroundColor(ThemeService.shared().isCurrentThemeDark() ? Color(uiColor: darkmodeBackground) : Color.black)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.background(Color(ThemeService.shared().theme.backgroundColor))
|
||||
.cornerRadius(12)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 25)
|
||||
}
|
||||
|
||||
var remindMeLaterButton: some View {
|
||||
Button {
|
||||
delegate?.didPressClose()
|
||||
} label: {
|
||||
HStack() {
|
||||
Spacer()
|
||||
Text(BWIL10n.bwiMobileDialogMBannerButton1)
|
||||
.font(.system(size: 18))
|
||||
.padding(10)
|
||||
.foregroundColor(Color(ThemeService.shared().theme.backgroundColor))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color(ThemeService.shared().theme.backgroundColor), lineWidth: 2))
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 25)
|
||||
}
|
||||
|
||||
313
bwi/FeatureBanner/MigrationInfoView.swift
Normal file
313
bwi/FeatureBanner/MigrationInfoView.swift
Normal file
@@ -0,0 +1,313 @@
|
||||
//
|
||||
/*
|
||||
* Copyright (c) 2025 BWI GmbH
|
||||
*
|
||||
* 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
|
||||
|
||||
// MARK: Migraion Info View
|
||||
struct MigrationInfoView: View {
|
||||
@Environment(\.dismiss) var dismissView
|
||||
@State private var selectedTab = 1
|
||||
@EnvironmentObject var themeService: BWIThemeService
|
||||
@State var redrawKey = UUID()
|
||||
|
||||
let username: String
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
MigrationInfoViewOne()
|
||||
.tag(1)
|
||||
MigrationInfoViewTwo(username: username)
|
||||
.tag(2)
|
||||
}
|
||||
.id(redrawKey)
|
||||
.tabViewStyle(.page)
|
||||
.tabViewStyle(.page(indexDisplayMode: .always))
|
||||
.background {
|
||||
Color(themeService.theme.backgroundColor)
|
||||
.edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
.onAppear() {
|
||||
FeatureBannerVisibilityService().markAsRead()
|
||||
NotificationCenter.default.post(name: .bwiMarkTopBannerAsRead, object: self, userInfo: ["type" : "feature_banner"])
|
||||
}
|
||||
.overlay {
|
||||
VStack() {
|
||||
HStack() {
|
||||
Spacer()
|
||||
Button {
|
||||
dismissView()
|
||||
} label: {
|
||||
Image(Asset.Images.closeButton.name)
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.frame(width: 36, height: 36)
|
||||
.foregroundColor(Color(themeService.theme.colors.tertiaryContent))
|
||||
.padding(20)
|
||||
}
|
||||
.accessibilityLabel(BWIL10n.bwiA11yCloseButton)
|
||||
}
|
||||
Spacer()
|
||||
HStack() {
|
||||
Button {
|
||||
withAnimation {
|
||||
selectedTab = 1
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.left")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundStyle(Color(themeService.theme.colors.tertiaryContent))
|
||||
.padding(EdgeInsets(top: 20, leading: 30, bottom: 10, trailing: 30))
|
||||
}
|
||||
.opacity(selectedTab == 1 ? 0 : 1)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
withAnimation {
|
||||
selectedTab = 2
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.right")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundStyle(Color(themeService.theme.colors.tertiaryContent))
|
||||
.padding(EdgeInsets(top: 20, leading: 30, bottom: 10, trailing: 30))
|
||||
}
|
||||
.opacity(selectedTab == 2 ? 0 : 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
setupIndicatorColors()
|
||||
}
|
||||
.onChange(of: themeService.isCurrentThemeDark) { _ in
|
||||
setupIndicatorColors()
|
||||
redrawKey = UUID()
|
||||
}
|
||||
.onChange(of: themeService.isCurrentThemeDark) { _ in
|
||||
setupIndicatorColors()
|
||||
redrawKey = UUID()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupIndicatorColors() {
|
||||
UIPageControl.appearance().currentPageIndicatorTintColor = themeService.theme.textPrimaryColor
|
||||
UIPageControl.appearance().pageIndicatorTintColor = themeService.theme.textSecondaryColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Migraion Info View one
|
||||
struct MigrationInfoViewOne: View {
|
||||
@EnvironmentObject var themeService: BWIThemeService
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
ScrollView(.vertical) {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Image("migration_detail_1")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: geo.size.height * 0.35, alignment: .bottom)
|
||||
.frame(maxWidth: geo.size.width - 60)
|
||||
.padding(EdgeInsets(top: 56, leading: 30, bottom: 0, trailing: 30))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
infoText
|
||||
.frame(width: geo.size.width)
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.frame(minHeight: geo.size.height)
|
||||
.frame(width: geo.size.width)
|
||||
}
|
||||
.frame(width: geo.size.width, height: geo.size.height)
|
||||
}
|
||||
}
|
||||
|
||||
var infoText: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(BWIL10n.bwiMobileDialogMMoreTitle)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.lineLimit(nil)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
Text(BWIL10n.bwiMobileDialogM1MoreText1) +
|
||||
Text(BWIL10n.bwiMobileDialogM1MoreText2).bold() +
|
||||
Text(BWIL10n.bwiMobileDialogM1MoreText3)
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .top) {
|
||||
Text("•")
|
||||
Text(BWIL10n.bwiMobileDialogM1MoreTextBullet1)
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
Text("•")
|
||||
Text(BWIL10n.bwiMobileDialogM1MoreTextBullet2)
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
Text("•")
|
||||
Text(BWIL10n.bwiMobileDialogM1MoreTextBullet3)
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
Text("•")
|
||||
Text(BWIL10n.bwiMobileDialogM1MoreTextBullet4)
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.leading, 16)
|
||||
}
|
||||
.lineLimit(nil)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(EdgeInsets(top: 10, leading: 30, bottom: 0, trailing: 30))
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(
|
||||
BWIL10n.bwiMobileDialogMMoreTitle + ". " +
|
||||
BWIL10n.bwiMobileDialogM1MoreText1 +
|
||||
BWIL10n.bwiMobileDialogM1MoreText2 +
|
||||
BWIL10n.bwiMobileDialogM1MoreText3 + ", " +
|
||||
BWIL10n.bwiMobileDialogM1MoreTextBullet1 + ", " +
|
||||
BWIL10n.bwiMobileDialogM1MoreTextBullet2 + ", " +
|
||||
BWIL10n.bwiMobileDialogM1MoreTextBullet3 + ", " +
|
||||
BWIL10n.bwiMobileDialogM1MoreTextBullet4)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Migraion Info View two
|
||||
struct MigrationInfoViewTwo: View {
|
||||
let username: String
|
||||
@State var showSuccessToast: Bool = false
|
||||
@EnvironmentObject var themeService: BWIThemeService
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
ScrollView(.vertical) {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
VStack() {
|
||||
Image("bumx_logo")
|
||||
.resizable()
|
||||
.frame(width: 200, height: 200)
|
||||
.clipShape(.rect(cornerRadius: 36))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 36)
|
||||
.stroke(.gray, lineWidth: 0.4)
|
||||
)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.frame(height: geo.size.height * 0.35, alignment: .center)
|
||||
.padding(EdgeInsets(top: 56, leading: 30, bottom: 10, trailing: 30))
|
||||
|
||||
instructionsList
|
||||
|
||||
successToast
|
||||
|
||||
Spacer()
|
||||
|
||||
downloadNewAppButton
|
||||
.frame(maxWidth: 350)
|
||||
.frame(width: geo.size.width - 100)
|
||||
.padding(.bottom, 100)
|
||||
}
|
||||
.frame(minHeight: geo.size.height)
|
||||
.frame(width: geo.size.width)
|
||||
|
||||
}
|
||||
.frame(width: geo.size.width, height: geo.size.height)
|
||||
}
|
||||
}
|
||||
|
||||
var instructionsList: some View {
|
||||
VStack() {
|
||||
VStack(alignment: .center) {
|
||||
Text(BWIL10n.bwiMobileDialogMMore2Title)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
HStack() {
|
||||
Text("1.")
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = username
|
||||
guard !showSuccessToast else { return }
|
||||
withAnimation {
|
||||
showSuccessToast = true
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
||||
withAnimation {
|
||||
showSuccessToast = false
|
||||
}
|
||||
}
|
||||
}, label: {
|
||||
Text(BWIL10n.bwiMobileDialogMMore2Text1)
|
||||
.underline()
|
||||
.foregroundColor(Color(themeService.theme.textPrimaryColor))
|
||||
Image(systemName: "square.on.square")
|
||||
.resizable()
|
||||
.foregroundColor(Color(themeService.theme.textPrimaryColor))
|
||||
.frame(width: 15, height: 15)
|
||||
})
|
||||
}
|
||||
|
||||
HStack() {
|
||||
Text("2.")
|
||||
Text(BWIL10n.bwiMobileDialogMMore2Text2)
|
||||
}
|
||||
.padding(.bottom, 1)
|
||||
HStack() {
|
||||
Text("3.")
|
||||
Text(BWIL10n.bwiMobileDialogMMore2Text3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.lineLimit(nil)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(EdgeInsets(top: 10, leading: 30, bottom: 0, trailing: 30))
|
||||
}
|
||||
|
||||
var downloadNewAppButton: some View {
|
||||
Button(action: {
|
||||
guard let bumxAppStoreURL = URL(string: "itms-apps://itunes.apple.com/app/id6738500048") else { return }
|
||||
guard UIApplication.shared.canOpenURL(bumxAppStoreURL) else { return }
|
||||
UIApplication.shared.open(bumxAppStoreURL, options: [:], completionHandler: nil)
|
||||
}, label: {
|
||||
Text(BWIL10n.bwiMobileDialogMMoreButton)
|
||||
.foregroundColor(Color(ThemeService.shared().theme.backgroundColor))
|
||||
})
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
}
|
||||
|
||||
var successToast: some View {
|
||||
HStack() {
|
||||
Image(systemName: "checkmark")
|
||||
.resizable()
|
||||
.frame(width: 15, height: 15)
|
||||
.accessibilityHidden(true)
|
||||
Text(BWIL10n.bwiMobileDialogMMoreSuccess)
|
||||
}
|
||||
.foregroundColor(themeService.isCurrentThemeDark ? .black : .white)
|
||||
.padding(10)
|
||||
.background(themeService.isCurrentThemeDark ? .white : .black)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.opacity(showSuccessToast ? 1 : 0)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ import UIKit
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
_ = FeatureBannerVisibilityService(mxSession: self.session).markAsRead(version: self.version)
|
||||
FeatureBannerVisibilityService().markAsRead()
|
||||
NotificationCenter.default.post(name: .bwiMarkTopBannerAsRead, object: self, userInfo: ["type" : "feature_banner"])
|
||||
}
|
||||
|
||||
|
||||
@@ -29,45 +29,26 @@ import Foundation
|
||||
static let featureVisibility = "de.bwi.should_show_ios_release_notes"
|
||||
}
|
||||
|
||||
private let session:MXSession
|
||||
|
||||
init(mxSession: MXSession) {
|
||||
self.session = mxSession
|
||||
func markAsRead() {
|
||||
BWIBuildSettings.shared.didShowBuMXMigrationInfoLevel = BWIBuildSettings.shared.BuMXMigrationInfoLevel
|
||||
}
|
||||
|
||||
func markAsRead( version: String ) -> MXHTTPOperation? {
|
||||
// Update only the "widgets" field in the account data
|
||||
var featureDict = self.session.accountData.accountData(forEventType: AccountDataTypes.featureVisibility) ?? [:]
|
||||
func markAsUnread( version: String ) {
|
||||
BWIBuildSettings.shared.didShowBuMXMigrationInfoLevel = 0
|
||||
}
|
||||
|
||||
func isUnread(completion: @escaping (_ unread : Bool) -> Void) {
|
||||
guard BWIBuildSettings.shared.BuMXMigrationInfoLevel < 3 else {
|
||||
completion(true)
|
||||
return
|
||||
}
|
||||
|
||||
featureDict[version] = false
|
||||
|
||||
return session.setAccountData(featureDict, forType: AccountDataTypes.featureVisibility, success: nil, failure: nil)
|
||||
}
|
||||
|
||||
func markAsUnread( version: String ) -> MXHTTPOperation? {
|
||||
// Update only the "widgets" field in the account data
|
||||
var featureDict = self.session.accountData.accountData(forEventType: AccountDataTypes.featureVisibility) ?? [:]
|
||||
|
||||
featureDict[version] = true
|
||||
|
||||
return session.setAccountData(featureDict, forType: AccountDataTypes.featureVisibility, success: {
|
||||
// bwi: update tableview
|
||||
NotificationCenter.default.post(name: .bwiMarkTopBannerAsUnRead, object: nil, userInfo: ["type" : "feature_banner"])
|
||||
}, failure: nil)
|
||||
}
|
||||
|
||||
func isUnread( version: String, completion: @escaping (_ unread : Bool) -> Void) {
|
||||
session.matrixRestClient.getAccountData(forType: AccountDataTypes.featureVisibility ) { (jsonResponse, error) in
|
||||
|
||||
guard let featureDict = jsonResponse as? [String:Any] else {
|
||||
completion(true)
|
||||
return
|
||||
}
|
||||
guard let unread = featureDict[version] as? Bool else {
|
||||
completion(true)
|
||||
return
|
||||
}
|
||||
completion(unread)
|
||||
if BWIBuildSettings.shared.didShowBuMXMigrationInfoLevel < BWIBuildSettings.shared.BuMXMigrationInfoLevel {
|
||||
completion(true)
|
||||
return
|
||||
} else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,24 @@ public extension MXWellKnown {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func migrationInfoLevel() -> Int {
|
||||
do {
|
||||
guard let bwiDict = self.jsonDictionary()["de.bwi"] as? [String : Any] else {
|
||||
return 0
|
||||
}
|
||||
|
||||
let bwi = try WellknownBWI(dict: bwiDict)
|
||||
if let migration = bwi.migrationBuMX {
|
||||
return migration.level ?? 0
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@objc func updateFederationStatus() {
|
||||
do {
|
||||
guard let bwiDict = self.jsonDictionary()["de.bwi"] as? [String : Any] else {
|
||||
|
||||
@@ -21,6 +21,7 @@ struct WellknownBWI: Decodable {
|
||||
let dataPrivacyURL: String?
|
||||
let imprintURL: String?
|
||||
let federation: WellknownFederation?
|
||||
let migrationBuMX: WellknownMigrationBuMX?
|
||||
|
||||
init(dict: [String: Any]) throws {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
|
||||
@@ -32,6 +33,7 @@ struct WellknownBWI: Decodable {
|
||||
case dataPrivacyURL = "data_privacy_url"
|
||||
case imprintURL = "imprint_url"
|
||||
case federation
|
||||
case migrationBuMX = "migration_bumx"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,3 +54,11 @@ struct WellknownFederation: Decodable {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct WellknownMigrationBuMX: Decodable {
|
||||
let level: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case level
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user