diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index be6c35bd..079e5483 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -41,6 +41,9 @@ 393411C723903C94003B49B8 /* EventCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393411C623903C94003B49B8 /* EventCollection.swift */; }; 393411C923904428003B49B8 /* MXEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393411C823904428003B49B8 /* MXEvent.swift */; }; 393411D1239087D2003B49B8 /* EventCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393411D0239087D2003B49B8 /* EventCollectionTests.swift */; }; + 3955DD31245B81A200827F07 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3955DD30245B81A200827F07 /* RegistrationView.swift */; }; + 3955DD33245B824E00827F07 /* LoginFormTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3955DD32245B824E00827F07 /* LoginFormTextField.swift */; }; + 3955DD35245B9D2100827F07 /* URL+Homeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3955DD34245B9D2100827F07 /* URL+Homeserver.swift */; }; 3970DC942385A8BE00EFE31B /* KeyboardObserving in Frameworks */ = {isa = PBXBuildFile; productRef = 3970DC932385A8BE00EFE31B /* KeyboardObserving */; }; 3984654523B7ECBA006C173B /* MXURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984654423B7ECBA006C173B /* MXURL.swift */; }; 3984654823B8D809006C173B /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3984654723B8D809006C173B /* SDWebImageSwiftUI */; }; @@ -133,6 +136,9 @@ 393411D0239087D2003B49B8 /* EventCollectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCollectionTests.swift; sourceTree = ""; }; 393411D2239087D2003B49B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3955DD36245C371C00827F07 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 3955DD30245B81A200827F07 /* RegistrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = ""; }; + 3955DD32245B824E00827F07 /* LoginFormTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFormTextField.swift; sourceTree = ""; }; + 3955DD34245B9D2100827F07 /* URL+Homeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Homeserver.swift"; sourceTree = ""; }; 3984654423B7ECBA006C173B /* MXURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXURL.swift; sourceTree = ""; }; 3997DCCF245732F000763C07 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; 39B834BF243FC42000AE1EA0 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; @@ -210,6 +216,8 @@ isa = PBXGroup; children = ( 39C931F423846966004449E1 /* LoginView.swift */, + 3955DD30245B81A200827F07 /* RegistrationView.swift */, + 3955DD32245B824E00827F07 /* LoginFormTextField.swift */, 3902B8A42395A77800698B87 /* LoadingView.swift */, ); path = Authentication; @@ -404,6 +412,7 @@ CAC46D6223A278F40079C24F /* PreviewProvider+Enumeration.swift */, 39BA0722240B3C9A00FD28C6 /* MXCredentials+Keychain.swift */, 39BA0726240B534600FD28C6 /* Color+allAccent.swift */, + 3955DD34245B9D2100827F07 /* URL+Homeserver.swift */, ); path = Extensions; sourceTree = ""; @@ -709,11 +718,13 @@ 392389892386FD3900B2E1DF /* MXClient+Publisher.swift in Sources */, 3984654523B7ECBA006C173B /* MXURL.swift in Sources */, 392389D2238F2E6F00B2E1DF /* NIORoom.swift in Sources */, + 3955DD33245B824E00827F07 /* LoginFormTextField.swift in Sources */, 4B058B5624573A570059BC75 /* EditEvent.swift in Sources */, 392221AE243A0508004D8794 /* GroupedReactionsView.swift in Sources */, 3923898F2388707E00B2E1DF /* RoomListItemView.swift in Sources */, 39C931DD2384328A004449E1 /* AppDelegate.swift in Sources */, 393411C923904428003B49B8 /* MXEvent.swift in Sources */, + 3955DD35245B9D2100827F07 /* URL+Homeserver.swift in Sources */, 3923898D238859D100B2E1DF /* MX+Identifiable.swift in Sources */, CAC46D5B23A2734C0079C24F /* EnvironmentValues.swift in Sources */, 39C932072384BB13004449E1 /* RecentRoomsView.swift in Sources */, @@ -733,6 +744,7 @@ 392389CC238EBB1500B2E1DF /* ReverseList.swift in Sources */, 392389942388899200B2E1DF /* Formatter.swift in Sources */, 39BA0723240B3C9A00FD28C6 /* MXCredentials+Keychain.swift in Sources */, + 3955DD31245B81A200827F07 /* RegistrationView.swift in Sources */, 392221AC2438149D004D8794 /* RoomMemberEventView.swift in Sources */, 392221B4243D1627004D8794 /* RoomPowerLevelsEventView.swift in Sources */, CAC46D6323A278F40079C24F /* PreviewProvider+Enumeration.swift in Sources */, diff --git a/Nio/AccountStore.swift b/Nio/AccountStore.swift index 840c294f..9fd23318 100644 --- a/Nio/AccountStore.swift +++ b/Nio/AccountStore.swift @@ -45,6 +45,33 @@ class AccountStore: ObservableObject { self.session?.removeListener(self.listenReference) } + // MARK: - Registration + + func register(username: String, password: String, homeserver: URL) { + self.loginState = .authenticating + + self.client = MXRestClient(homeServer: homeserver, unrecognizedCertificateHandler: nil) + self.client?.register(username: username, password: password) { response in + switch response { + case .failure(let error): + self.loginState = .failure(error) + case .success(let credentials): + self.credentials = credentials + credentials.save(to: self.keychain) + + self.sync { result in + switch result { + case .failure(let error): + // Does this make sense? The login itself didn't fail, but syncing did. + self.loginState = .failure(error) + case .success(let state): + self.loginState = state + } + } + } + } + } + // MARK: - Login & Sync @Published var loginState: LoginState = .loggedOut diff --git a/Nio/Authentication/LoginFormTextField.swift b/Nio/Authentication/LoginFormTextField.swift new file mode 100644 index 00000000..77a68aa6 --- /dev/null +++ b/Nio/Authentication/LoginFormTextField.swift @@ -0,0 +1,51 @@ +import SwiftUI + +struct LoginFormTextField: View { + @Environment(\.colorScheme) var colorScheme + + var placeholder: String + @Binding var text: String + var onEditingChanged: ((Bool) -> Void)? + + var isSecure = false + + var buttonIcon: String? + var buttonAction: (() -> Void)? + + var body: some View { + ZStack { + Capsule(style: .continuous) + .foregroundColor(colorScheme == .light ? Color(#colorLiteral(red: 0.9395676295, green: 0.9395676295, blue: 0.9395676295, alpha: 1)) : Color(#colorLiteral(red: 0.2293992357, green: 0.2293992357, blue: 0.2293992357, alpha: 1))) + .frame(height: 50) + if isSecure { + SecureField(placeholder, text: $text) + .padding() + .textContentType(.password) + } else { + HStack { + TextField(placeholder, text: $text, onEditingChanged: onEditingChanged ?? { _ in }) + .padding() + .autocapitalization(.none) + .disableAutocorrection(true) + if buttonIcon != nil && buttonAction != nil { + Button(action: { + self.buttonAction!() + }, label: { + Image(systemName: buttonIcon!) + }) + .padding() + } + } + } + } + .frame(maxWidth: 400) + } +} + +struct LoginFormTextField_Previews: PreviewProvider { + static var previews: some View { + LoginFormTextField(placeholder: "Username", text: .constant("")) + .padding() + .previewLayout(.sizeThatFits) + } +} diff --git a/Nio/Authentication/LoginView.swift b/Nio/Authentication/LoginView.swift index 218f3ae0..287a87bb 100644 --- a/Nio/Authentication/LoginView.swift +++ b/Nio/Authentication/LoginView.swift @@ -3,6 +3,7 @@ import SwiftMatrixSDK struct LoginContainerView: View { @EnvironmentObject var store: AccountStore + @EnvironmentObject var settings: AppSettings @State private var username = "" @State private var password = "" @@ -18,18 +19,16 @@ struct LoginContainerView: View { isLoginEnabled: isLoginEnabled, onLogin: login, guessHomeserverURL: guessHomeserverURL) + .sheet(isPresented: $showingRegisterView) { + RegistrationContainerView() + .accentColor(self.settings.accentColor) + .environmentObject(self.store) + } } private func login() { - var homeserver = self.homeserver.isEmpty ? "https://matrix.org" : self.homeserver - - // If there's no scheme at all, the URLComponents initializer below will think it's a path with no hostname. - if !homeserver.contains("//") { - homeserver = "https://\(homeserver)" - } - var homeserverURLComponents = URLComponents(string: homeserver) - homeserverURLComponents?.scheme = "https" - guard let homeserverURL = homeserverURLComponents?.url else { + let homeserver = self.homeserver.isEmpty ? "https://matrix.org" : self.homeserver + guard let homeserverURL = URL(homeserverString: homeserver) else { // TODO: Handle error print("Invalid homeserver URL '\(homeserver)'") return @@ -86,9 +85,6 @@ struct LoginView: View { Spacer() } .keyboardObserving() - .sheet(isPresented: $showingRegisterView) { - Text(L10n.Login.registerNotYetImplemented) - } } var buttons: some View { @@ -117,6 +113,7 @@ struct LoginTitleView: View { let nio = Text("Nio").foregroundColor(.accentColor) return VStack { + // FIXME: This probably breaks localisation. (Text(L10n.Login.welcomeHeader) + nio + Text("!")) .font(.title) .bold() @@ -129,52 +126,27 @@ struct LoginForm: View { @Binding var username: String @Binding var password: String @Binding var homeserver: String - + let guessHomeserverURL: () -> Void var body: some View { VStack { - FormTextField(title: L10n.Login.Form.username, text: $username, onEditingChanged: { _ in - self.guessHomeserverURL() - }) + LoginFormTextField(placeholder: L10n.Login.Form.username, + text: $username, + onEditingChanged: { _ in self.guessHomeserverURL() }) - FormTextField(title: L10n.Login.Form.password, text: $password, isSecure: true) + LoginFormTextField(placeholder: L10n.Login.Form.password, + text: $password, + isSecure: true) + + LoginFormTextField(placeholder: L10n.Login.Form.homeserver, + text: $homeserver) - FormTextField(title: L10n.Login.Form.homeserver, text: $homeserver) Text(L10n.Login.Form.homeserverOptionalExplanation) .font(.caption) .foregroundColor(.gray) } - } -} - -private struct FormTextField: View { - @Environment(\.colorScheme) var colorScheme - - var title: String - @Binding var text: String - var onEditingChanged: ((Bool) -> Void)? - - var isSecure = false - - var body: some View { - ZStack { - Capsule(style: .continuous) - .foregroundColor(colorScheme == .light ? Color(#colorLiteral(red: 0.9395676295, green: 0.9395676295, blue: 0.9395676295, alpha: 1)) : Color(#colorLiteral(red: 0.2293992357, green: 0.2293992357, blue: 0.2293992357, alpha: 1))) - .frame(height: 50) - if isSecure { - SecureField(title, text: $text) - .padding() - .textContentType(.password) - } else { - TextField(title, text: $text, onEditingChanged: onEditingChanged ?? { _ in }) - .padding() - .autocapitalization(.none) - .disableAutocorrection(true) - } - } .padding(.horizontal) - .frame(maxWidth: 400) } } diff --git a/Nio/Authentication/RegistrationView.swift b/Nio/Authentication/RegistrationView.swift new file mode 100644 index 00000000..8401bb31 --- /dev/null +++ b/Nio/Authentication/RegistrationView.swift @@ -0,0 +1,164 @@ +import SwiftUI + +struct RegistrationContainerView: View { + @EnvironmentObject var store: AccountStore + + @State private var username = "" + @State private var password = "" + @State private var passwordConfirmation = "" + @State private var homeserver = "" + + private func register() { + let homeserver = self.homeserver.isEmpty ? "https://matrix.org" : self.homeserver + guard let homeserverURL = URL(homeserverString: homeserver) else { + // TODO: Handle error + print("Invalid homeserver URL '\(homeserver)'") + return + } + store.register(username: username, password: password, homeserver: homeserverURL) + } + + var body: some View { + RegistrationView(username: $username, + password: $password, + passwordConfirmation: $passwordConfirmation, + homeserver: $homeserver, + onRegister: register, + isRegistrationEnabled: isRegistrationEnabled) + } + + private func isRegistrationEnabled() -> Bool { + guard !username.isEmpty && !password.isEmpty else { return false } + guard password == passwordConfirmation else { return false } + let homeserver = self.homeserver.isEmpty ? "https://matrix.org" : self.homeserver + guard URL(string: homeserver) != nil else { return false } + return true + } +} + +struct RegistrationView: View { + @Binding var username: String + @Binding var password: String + @Binding var passwordConfirmation: String + @Binding var homeserver: String + + var onRegister: () -> Void + var isRegistrationEnabled: () -> Bool + + static var randomServerSuggestions = [ + "https://feneas.org", + "https://allmende.io", + "https://tchncs.de", + "https://fairydust.space", + ] + + var header: some View { + VStack { + Image(systemName: "person.3.fill") + .font(.title) + .foregroundColor(.accentColor) + Text(L10n.Registration.header) + .font(.headline) + } + .padding(.bottom) + } + + var mxidPreview: String? { + // TODO: This should ideally also try a well-known discovery like the login does. + switch (username, homeserver) { + case ("", _): + return nil + case (var user, ""): + user = user.replacingOccurrences(of: "@", with: "") + return "@\(user):matrix.org" + case (var user, var server): + user = user.replacingOccurrences(of: "@", with: "") + server = server.replacingOccurrences(of: "https://", with: "") + return "@\(user):\(server)" + } + } + + var form: some View { + VStack { + LoginFormTextField(placeholder: L10n.Login.Form.username, text: $username) + .padding(.horizontal) + .padding(.bottom) + + LoginFormTextField(placeholder: L10n.Login.Form.password, + text: $password, + isSecure: true) + .padding(.horizontal) + + LoginFormTextField(placeholder: L10n.Registration.confirmPassword, + text: $passwordConfirmation, + isSecure: true) + .padding(.horizontal) + .padding(.bottom) + + LoginFormTextField(placeholder: L10n.Login.Form.homeserver, + text: $homeserver, + buttonIcon: "shuffle", + buttonAction: { + self.homeserver = Self.randomServerSuggestions + .filter { $0 != self.homeserver } + .randomElement()! + } + ) + .padding(.horizontal) + } + } + + var body: some View { + VStack { + Spacer() + header + + Spacer() + Text(L10n.Registration.explanation) + .font(.callout) + .padding(.horizontal) + Spacer() + + form + + Text(L10n.Registration.homeserverExplanation) + .font(.caption) + .foregroundColor(.gray) + .padding(.horizontal) + .fixedSize(horizontal: false, vertical: true) + + Button(action: { + self.onRegister() + }, label: { + VStack { + Text(L10n.Registration.register) + .font(.system(size: 18)) + .bold() + if mxidPreview != nil { + Text(mxidPreview!) + .font(.caption) + .bold() + } + } + + }) + .padding([.top, .bottom], 30) + .disabled(!isRegistrationEnabled()) + + Spacer() + } + .keyboardObserving() + } +} + +struct RegistrationView_Previews: PreviewProvider { + static var previews: some View { + RegistrationView(username: .constant(""), + password: .constant(""), + passwordConfirmation: .constant(""), + homeserver: .constant(""), + onRegister: { }, + isRegistrationEnabled: { true }) + .accentColor(.purple) + } +} diff --git a/Nio/Extensions/URL+Homeserver.swift b/Nio/Extensions/URL+Homeserver.swift new file mode 100644 index 00000000..34b17f76 --- /dev/null +++ b/Nio/Extensions/URL+Homeserver.swift @@ -0,0 +1,18 @@ +import Foundation + +extension URL { + init?(homeserverString: String) { + var homeserver = homeserverString + + // If there's no scheme at all, the URLComponents initializer below will think it's a path with no hostname. + if !homeserver.contains("//") { + homeserver = "https://\(homeserver)" + } + + var homeserverURLComponents = URLComponents(string: homeserver) + homeserverURLComponents?.scheme = "https" + + guard let homeserverURL = homeserverURLComponents?.url else { return nil } + self = homeserverURL + } +} diff --git a/Nio/Generated/Strings.swift b/Nio/Generated/Strings.swift index 1d528aa4..b62520e6 100644 --- a/Nio/Generated/Strings.swift +++ b/Nio/Generated/Strings.swift @@ -146,8 +146,6 @@ internal enum L10n { internal static let failureBackToLogin = L10n.tr("Localizable", "login.failure-back-to-login") /// Don't have an account yet? internal static let openRegistrationPrompt = L10n.tr("Localizable", "login.open-registration-prompt") - /// Registering for new accounts is not yet implemented. - internal static let registerNotYetImplemented = L10n.tr("Localizable", "login.register-not-yet-implemented") /// Sign in internal static let signIn = L10n.tr("Localizable", "login.sign-in") /// 👋 Welcome to @@ -194,6 +192,19 @@ internal enum L10n { } } + internal enum Registration { + /// Confirm password + internal static let confirmPassword = L10n.tr("Localizable", "registration.confirm-password") + /// Matrix is a decentralized network, like E-Mail, meaning there's no single server but many that talk to each other. You'll need an account on one of them to talk to other users. + internal static let explanation = L10n.tr("Localizable", "registration.explanation") + /// Register a new Matrix account + internal static let header = L10n.tr("Localizable", "registration.header") + /// You can use this to create an account on a specific Matrix server. Tap the shuffle button to get a random suggestion or leave it empty to create your account on matrix.org. + internal static let homeserverExplanation = L10n.tr("Localizable", "registration.homeserver-explanation") + /// Register + internal static let register = L10n.tr("Localizable", "registration.register") + } + internal enum Room { /// Not yet implemented internal static let attachmentPlaceholder = L10n.tr("Localizable", "room.attachment-placeholder") diff --git a/Nio/RootView.swift b/Nio/RootView.swift index d2247ed3..ba17b68f 100644 --- a/Nio/RootView.swift +++ b/Nio/RootView.swift @@ -10,7 +10,7 @@ struct RootView: View { RecentRoomsContainerView() .environment(\.userId, userId) // Can this ever be nil? And if so, what happens with the default fallback? - .environment(\.homeserver, (store.client?.homeserver.flatMap(URL.init)) ?? HomeserverKey.defaultValue) + .environment(\.homeserver, (store.client?.homeserver.flatMap(URL.init(string:))) ?? HomeserverKey.defaultValue) ) case .loggedOut: return AnyView( diff --git a/Nio/Supporting Files/de.lproj/Localizable.strings b/Nio/Supporting Files/de.lproj/Localizable.strings index d72f2cf4..d81c48c0 100644 --- a/Nio/Supporting Files/de.lproj/Localizable.strings +++ b/Nio/Supporting Files/de.lproj/Localizable.strings @@ -6,9 +6,14 @@ "login.form.homeserver-optional-explanation" = "Der Homeserver ist optional falls du auf matrix.org registriert bist."; "login.sign-in" = "Anmelden"; "login.open-registration-prompt" = "Du hast noch kein Benutzerkonto?"; -"login.register-not-yet-implemented" = "Registrierung für neue Konten ist noch nicht implementiert."; "login.failure-back-to-login" = "Zurück zur Anmeldung"; +"registration.header" = "Erstelle ein neues Matrix-Konto"; +"registration.explanation" = "Matrix ist ein dezentralisiertes Netzwerk, wie E-Mail, d.h. es gibt keinen einzelnen Server, sondern viele, die miteinander kommunizieren. Du brauchst ein Konto auf einem davon, um mit anderen Benutzern zu kommunizieren."; +"registration.confirm-password" = "Passwort bestätigen"; +"registration.homeserver-explanation" = "Du kannst dieses Feld verwenden, um ein Konto auf einem bestimmten Matrix-Server zu erstellen. Tippe auf den Shuffle-Button, um einen Zufallsvorschlag zu erhalten, oder lasse das Feld leer, um ein Konto auf matrix.org zu erstellen."; +"registration.register" = "Registrieren"; + "loading.1" = "🧑‍🎤 Die Matrix wird entschlüsselt"; "loading.2" = "🧑‍🏭 Nachrichten werden auseinander gefriemelt"; "loading.3" = "🧑‍🔧 Anmeldung läuft"; diff --git a/Nio/Supporting Files/en.lproj/Localizable.strings b/Nio/Supporting Files/en.lproj/Localizable.strings index 37bdd237..238c7a80 100644 --- a/Nio/Supporting Files/en.lproj/Localizable.strings +++ b/Nio/Supporting Files/en.lproj/Localizable.strings @@ -6,9 +6,14 @@ "login.form.homeserver-optional-explanation" = "Homeserver is optional if you're using matrix.org."; "login.sign-in" = "Sign in"; "login.open-registration-prompt" = "Don't have an account yet?"; -"login.register-not-yet-implemented" = "Registering for new accounts is not yet implemented."; "login.failure-back-to-login" = "Back to Login"; +"registration.header" = "Register a new Matrix account"; +"registration.explanation" = "Matrix is a decentralized network, like E-Mail, meaning there's no single server but many that talk to each other. You'll need an account on one of them to talk to other users."; +"registration.confirm-password" = "Confirm password"; +"registration.homeserver-explanation" = "You can use this to create an account on a specific Matrix server. Tap the shuffle button to get a random suggestion or leave it empty to create your account on matrix.org."; +"registration.register" = "Register"; + "loading.1" = "🧑‍🎤 Reticulating splines"; "loading.2" = "🧑‍🏭 Discomfrobulating messages"; "loading.3" = "🧑‍🔧 Logging in"; diff --git a/Nio/Supporting Files/nl.lproj/Localizable.strings b/Nio/Supporting Files/nl.lproj/Localizable.strings index fac8948d..876e65c2 100644 --- a/Nio/Supporting Files/nl.lproj/Localizable.strings +++ b/Nio/Supporting Files/nl.lproj/Localizable.strings @@ -6,9 +6,14 @@ "login.form.homeserver-optional-explanation" = "Homeserver is optioneel als je matrix.org gebruikt."; "login.sign-in" = "Log in"; "login.open-registration-prompt" = "Nog geen account?"; -"login.register-not-yet-implemented" = "Registratie voor nieuwe accounts is nog niet geïmplementeerd."; "login.failure-back-to-login" = "Terug naar Login"; +"registration.header" = "Registreer een nieuwe Matrix account"; +"registration.explanation" = "Matrix is een gedecentraliseerd netwerk, zoals E-Mail, wat betekent dat er niet één enkele server is, maar meerdere die met elkaar praten. Je hebt een account nodig op één van hen om met andere gebruikers te kunnen praten."; +"registration.confirm-password" = "Bevestig wachtwoord"; +"registration.homeserver-explanation" = "U kunt dit gebruiken om een account aan te maken op een specifieke Matrix-server. Tik op de shuffle-knop om een willekeurige suggestie te krijgen of laat het leeg om uw account aan te maken op matrix.org."; +"registration.register" = "Registreer"; + "loading.1" = "🧑‍🎤 De Matrix wordt gedecodeerd"; "loading.2" = "🧑‍🏭 Berichten worden verzameld"; "loading.3" = "🧑‍🔧 Inloggen"; diff --git a/Nio/Supporting Files/zh-Hans.lproj/Localizable.strings b/Nio/Supporting Files/zh-Hans.lproj/Localizable.strings index b93e35b9..16d369df 100644 --- a/Nio/Supporting Files/zh-Hans.lproj/Localizable.strings +++ b/Nio/Supporting Files/zh-Hans.lproj/Localizable.strings @@ -6,9 +6,14 @@ "login.form.homeserver-optional-explanation" = "若是matrix.org,则不必填写主服务器。"; "login.sign-in" = "登录"; "login.open-registration-prompt" = "还没有账户?"; -"login.register-not-yet-implemented" = "新账户注册功能尚未被实现。"; "login.failure-back-to-login" = "返回登录页面"; +"registration.header" = "注册一个新的Matrix账户"; +"registration.explanation" = "Matrix就像电子邮箱一样,是一个分散式的网络。这意味着没有一个唯一的服务器,而是有很多服务器在相互对话。你只须在其中一个服务器上拥有账号就能与其他用户交流。"; +"registration.confirm-password" = "确认密码"; +"registration.homeserver-explanation" = "你可以用它来在特定的Matrix服务器上创建账户。点击􀊝来获得随机的服务器建议,或留空以在matrix.org上创建账户。"; +"registration.register" = "注册"; + "loading.1" = "🧑‍🎤 少女祈祷中"; "loading.2" = "🧑‍🏭 正在给长者+1s"; "loading.3" = "🧑‍🔧 登录中"; @@ -27,7 +32,7 @@ "room.attachment-placeholder" = "此特性尚未被实现。"; "room.remove.title" = "删除?"; "room.remove.message" = "你确定要删除这条消息吗?"; -"room.remove.action" = "删除";; +"room.remove.action" = "删除"; "composer.new-message" = "新信息..."; "composer.edit-message" = "编辑信息:";