Automatically remove spaces in server names (#1600)

* Automatically remove spaces in server names

If a server name includes a space (which can happen if the string is pasted /
autocompleted), this space is removed, which results in the app not crashing.
Fixes #1599

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

* Format

---------

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>
Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
Paul Schuetz 2023-10-01 09:37:09 +02:00 committed by GitHub
parent d32c5c004c
commit 0b5e764556
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 135 additions and 99 deletions

View file

@ -31,12 +31,12 @@ extension View {
case let .conversationDetail(conversation):
ConversationDetailView(conversation: conversation)
case let .hashTag(tag, accountId):
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)),
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)),
selectedTagGroup: .constant(nil),
scrollToTopSignal: .constant(0),
canFilterTimeline: false)
case let .list(list):
TimelineView(timeline: .constant(.list(list: list)),
TimelineView(timeline: .constant(.list(list: list)),
selectedTagGroup: .constant(nil),
scrollToTopSignal: .constant(0),
canFilterTimeline: false)
@ -131,12 +131,12 @@ extension View {
.environment(PushNotificationsService.shared)
.environment(AppAccountsManager.shared.currentClient)
}
func withModelContainer() -> some View {
modelContainer(for: [
Draft.self,
LocalTimeline.self,
TagGroup.self
TagGroup.self,
])
}
}

View file

@ -6,9 +6,9 @@ import Env
import KeychainSwift
import Network
import RevenueCat
import Status
import SwiftUI
import Timeline
import Status
@main
struct IceCubesApp: App {
@ -217,7 +217,7 @@ struct IceCubesApp: App {
case .active:
watcher.watch(streams: [.user, .direct])
UNUserNotificationCenter.current().setBadgeCount(0)
userPreferences.reloadNotificationsCount(tokens: appAccountsManager.availableAccounts.compactMap{ $0.oauthToken })
userPreferences.reloadNotificationsCount(tokens: appAccountsManager.availableAccounts.compactMap(\.oauthToken))
Task {
await userPreferences.refreshServerPreferences()
}
@ -286,10 +286,9 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult {
UserPreferences.shared.reloadNotificationsCount(tokens: AppAccountsManager.shared.availableAccounts.compactMap{ $0.oauthToken })
func application(_: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
UserPreferences.shared.reloadNotificationsCount(tokens: AppAccountsManager.shared.availableAccounts.compactMap(\.oauthToken))
return .noData
}

View file

@ -44,6 +44,10 @@ struct AddAccountView: View {
@FocusState private var isInstanceURLFieldFocused: Bool
private func cleanServerStr(_ server: String) -> String {
server.replacingOccurrences(of: " ", with: "")
}
var body: some View {
NavigationStack {
Form {
@ -54,6 +58,9 @@ struct AddAccountView: View {
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($isInstanceURLFieldFocused)
.onChange(of: instanceName) { _, _ in
instanceName = cleanServerStr(instanceName)
}
if let instanceFetchError {
Text(instanceFetchError)
}

View file

@ -6,9 +6,9 @@ import Foundation
import Models
import Network
import Nuke
import SwiftData
import SwiftUI
import Timeline
import SwiftData
@MainActor
struct SettingsTabs: View {
@ -29,7 +29,7 @@ struct SettingsTabs: View {
@State private var timelineCache = TimelineCache()
@Binding var popToRootTab: Tab
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]

View file

@ -43,7 +43,7 @@ struct TranslationSettingsView: View {
}
.onAppear(perform: updatePrefs)
}
@ViewBuilder
private var deepLToggle: some View {
@Bindable var preferences = preferences
@ -52,7 +52,7 @@ struct TranslationSettingsView: View {
}
.listRowBackground(theme.primaryBackgroundColor)
}
@ViewBuilder
private var deepLPicker: some View {
@Bindable var preferences = preferences
@ -61,7 +61,7 @@ struct TranslationSettingsView: View {
Text("DeepL API Pro").tag(false)
}
}
@ViewBuilder
private var autoDetectSection: some View {
@Bindable var preferences = preferences

View file

@ -22,7 +22,7 @@ struct EditTagGroupView: View {
private var editingTagGroup: TagGroup?
private var onSaved: ((TagGroup) -> Void)?
private var canSave: Bool {
!title.isEmpty &&
// At least have 2 tags, one main and one additional.

View file

@ -4,14 +4,14 @@ import DesignSystem
import Env
import Models
import Network
import SwiftData
import SwiftUI
import Timeline
import SwiftData
@MainActor
struct TimelineTab: View {
@Environment(\.modelContext) private var context
@Environment(AppAccountsManager.self) private var appAccount
@Environment(Theme.self) private var theme
@Environment(CurrentAccount.self) private var currentAccount
@ -24,13 +24,13 @@ struct TimelineTab: View {
@State private var timeline: TimelineFilter = .home
@State private var selectedTagGroup: TagGroup?
@State private var scrollToTopSignal: Int = 0
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
@AppStorage("remote_local_timeline") var legacyLocalTimelines: [String] = []
@AppStorage("tag_groups") var legacyTagGroups: [LegacyTagGroup] = []
@AppStorage("last_timeline_filter") var lastTimelineFilter: TimelineFilter = .home
private let canFilterTimeline: Bool
@ -249,7 +249,7 @@ struct TimelineTab: View {
}
}
}
private func resetTimelineFilter() {
if client.isAuth, canFilterTimeline {
timeline = lastTimelineFilter
@ -257,14 +257,14 @@ struct TimelineTab: View {
timeline = .federated
}
}
func migrateUserPreferencesTimeline() {
for instance in legacyLocalTimelines {
context.insert(LocalTimeline(instance: instance))
}
legacyLocalTimelines = []
}
func migrateUserPreferencesTagGroups() {
for group in legacyTagGroups {
context.insert(TagGroup(title: group.title, symbolName: group.sfSymbolName, tags: group.tags))

View file

@ -66,7 +66,7 @@ class NotificationService: UNNotificationServiceExtension {
let preferences = UserPreferences.shared
let tokens = AppAccountsManager.shared.pushAccounts.map(\.token)
preferences.reloadNotificationsCount(tokens: tokens)
if let token = AppAccountsManager.shared.availableAccounts.first(where: { $0.oauthToken?.accessToken == notification.accessToken })?.oauthToken {
var currentCount = preferences.notificationsCount[token] ?? 0
currentCount += 1

View file

@ -105,11 +105,10 @@ struct ConversationMessageView: View {
Button {
Task {
do {
let status: Status
if isLiked {
status = try await client.post(endpoint: Statuses.unfavorite(id: message.id))
let status: Status = if isLiked {
try await client.post(endpoint: Statuses.unfavorite(id: message.id))
} else {
status = try await client.post(endpoint: Statuses.favorite(id: message.id))
try await client.post(endpoint: Statuses.favorite(id: message.id))
}
withAnimation {
isLiked = status.favourited == true
@ -122,11 +121,10 @@ struct ConversationMessageView: View {
}
Button { Task {
do {
let status: Status
if isBookmarked {
status = try await client.post(endpoint: Statuses.unbookmark(id: message.id))
let status: Status = if isBookmarked {
try await client.post(endpoint: Statuses.unbookmark(id: message.id))
} else {
status = try await client.post(endpoint: Statuses.bookmark(id: message.id))
try await client.post(endpoint: Statuses.bookmark(id: message.id))
}
withAnimation {
isBookmarked = status.bookmarked == true

View file

@ -18,8 +18,8 @@ import SwiftUI
@AppStorage(ThemeKey.primaryBackground.rawValue) public var primaryBackgroundColor: Color = .white
@AppStorage(ThemeKey.secondaryBackground.rawValue) public var secondaryBackgroundColor: Color = .gray
@AppStorage(ThemeKey.label.rawValue) public var labelColor: Color = .black
@AppStorage(ThemeKey.avatarPosition2.rawValue) var avatarPosition: AvatarPosition = AvatarPosition.top
@AppStorage(ThemeKey.avatarShape2.rawValue) var avatarShape: AvatarShape = AvatarShape.rounded
@AppStorage(ThemeKey.avatarPosition2.rawValue) var avatarPosition: AvatarPosition = .top
@AppStorage(ThemeKey.avatarShape2.rawValue) var avatarShape: AvatarShape = .rounded
@AppStorage(ThemeKey.selectedSet.rawValue) var storedSet: ColorSetName = .iceCubeDark
@AppStorage(ThemeKey.statusActionsDisplay.rawValue) public var statusActionsDisplay: StatusActionsDisplay = .full
@AppStorage(ThemeKey.statusDisplayStyle.rawValue) public var statusDisplayStyle: StatusDisplayStyle = .large
@ -273,7 +273,7 @@ import SwiftUI
ConstellationDark(),
]
}
public func applySet(set: ColorSetName) {
selectedSet = set
setColor(withName: set)

View file

@ -11,10 +11,10 @@ import SwiftUI
@AppStorage("show_translate_button_inline") public var showTranslateButton: Bool = true
@AppStorage("show_pending_at_bottom") public var pendingShownAtBottom: Bool = false
@AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true
@AppStorage("recently_used_languages") public var recentlyUsedLanguages: [String] = []
@AppStorage("social_keyboard_composer") public var isSocialKeyboardEnabled: Bool = true
@AppStorage("use_instance_content_settings") public var useInstanceContentSettings: Bool = true
@AppStorage("app_auto_expand_spoilers") public var appAutoExpandSpoilers = false
@AppStorage("app_auto_expand_media") public var appAutoExpandMedia: ServerPreferences.AutoExpandMedia = .hideSensitive
@ -25,41 +25,41 @@ import SwiftUI
@AppStorage("always_use_deepl") public var alwaysUseDeepl = false
@AppStorage("user_deepl_api_free") public var userDeeplAPIFree = true
@AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true
@AppStorage("suppress_dupe_reblogs") public var suppressDupeReblogs: Bool = false
@AppStorage("inAppBrowserReaderView") public var inAppBrowserReaderView = false
@AppStorage("haptic_tab") public var hapticTabSelectionEnabled = true
@AppStorage("haptic_timeline") public var hapticTimelineEnabled = true
@AppStorage("haptic_button_press") public var hapticButtonPressEnabled = true
@AppStorage("sound_effect_enabled") public var soundEffectEnabled = true
@AppStorage("show_tab_label_iphone") public var showiPhoneTabLabel = true
@AppStorage("show_alt_text_for_media") public var showAltTextForMedia = true
@AppStorage("show_second_column_ipad") public var showiPadSecondaryColumn = true
@AppStorage("swipeactions-status-trailing-right") public var swipeActionsStatusTrailingRight = StatusAction.favorite
@AppStorage("swipeactions-status-trailing-left") public var swipeActionsStatusTrailingLeft = StatusAction.boost
@AppStorage("swipeactions-status-leading-left") public var swipeActionsStatusLeadingLeft = StatusAction.reply
@AppStorage("swipeactions-status-leading-right") public var swipeActionsStatusLeadingRight = StatusAction.none
@AppStorage("swipeactions-use-theme-color") public var swipeActionsUseThemeColor = false
@AppStorage("swipeactions-icon-style") public var swipeActionsIconStyle: SwipeActionsIconStyle = .iconWithText
@AppStorage("requested_review") public var requestedReview = false
@AppStorage("collapse-long-posts") public var collapseLongPosts = true
@AppStorage("share-button-behavior") public var shareButtonBehavior: PreferredShareButtonBehavior = .linkAndText
init() { }
init() {}
}
public static let sharedDefault = UserDefaults(suiteName: "group.com.thomasricouard.IceCubesApp")
public static let shared = UserPreferences()
private let storage = Storage()
private var client: Client?
public var preferredBrowser: PreferredBrowser {
@ -73,171 +73,202 @@ import SwiftUI
storage.showTranslateButton = showTranslateButton
}
}
public var pendingShownAtBottom : Bool {
didSet {
storage.pendingShownAtBottom = pendingShownAtBottom
}
}
public var isOpenAIEnabled: Bool {
didSet {
storage.isOpenAIEnabled = isOpenAIEnabled
}
}
public var recentlyUsedLanguages: [String] {
didSet {
storage.recentlyUsedLanguages = recentlyUsedLanguages
}
}
public var isSocialKeyboardEnabled: Bool {
didSet {
storage.isSocialKeyboardEnabled = isSocialKeyboardEnabled
}
}
public var useInstanceContentSettings: Bool {
didSet {
storage.useInstanceContentSettings = useInstanceContentSettings
}
}
public var appAutoExpandSpoilers: Bool {
didSet {
storage.appAutoExpandSpoilers = appAutoExpandSpoilers
}
}
public var appAutoExpandMedia: ServerPreferences.AutoExpandMedia {
didSet {
storage.appAutoExpandMedia = appAutoExpandMedia
}
}
public var appDefaultPostVisibility: Models.Visibility {
didSet {
storage.appDefaultPostVisibility = appDefaultPostVisibility
}
}
public var appDefaultReplyVisibility: Models.Visibility {
didSet {
storage.appDefaultReplyVisibility = appDefaultReplyVisibility
}
}
public var appDefaultPostsSensitive: Bool {
didSet {
storage.appDefaultPostsSensitive = appDefaultPostsSensitive
}
}
public var autoPlayVideo: Bool {
didSet {
storage.autoPlayVideo = autoPlayVideo
}
}
public var alwaysUseDeepl: Bool {
didSet {
storage.alwaysUseDeepl = alwaysUseDeepl
}
}
public var userDeeplAPIFree: Bool {
didSet {
storage.userDeeplAPIFree = userDeeplAPIFree
}
}
public var autoDetectPostLanguage: Bool {
didSet {
storage.autoDetectPostLanguage = autoDetectPostLanguage
}
}
public var suppressDupeReblogs: Bool {
didSet {
storage.suppressDupeReblogs = suppressDupeReblogs
}
}
public var inAppBrowserReaderView: Bool {
didSet {
storage.inAppBrowserReaderView = inAppBrowserReaderView
}
}
public var hapticTabSelectionEnabled: Bool {
didSet {
storage.hapticTabSelectionEnabled = hapticTabSelectionEnabled
}
}
public var hapticTimelineEnabled: Bool {
didSet {
storage.hapticTimelineEnabled = hapticTimelineEnabled
}
}
public var hapticButtonPressEnabled: Bool {
didSet {
storage.hapticButtonPressEnabled = hapticButtonPressEnabled
}
}
public var soundEffectEnabled: Bool {
didSet {
storage.soundEffectEnabled = soundEffectEnabled
}
}
public var showiPhoneTabLabel: Bool {
didSet {
storage.showiPhoneTabLabel = showiPhoneTabLabel
}
}
public var showAltTextForMedia: Bool {
didSet {
storage.showAltTextForMedia = showAltTextForMedia
}
}
public var showiPadSecondaryColumn: Bool {
didSet {
storage.showiPadSecondaryColumn = showiPadSecondaryColumn
}
}
public var swipeActionsStatusTrailingRight: StatusAction {
didSet {
storage.swipeActionsStatusTrailingRight = swipeActionsStatusTrailingRight
}
}
public var swipeActionsStatusTrailingLeft: StatusAction {
didSet {
storage.swipeActionsStatusTrailingLeft = swipeActionsStatusTrailingLeft
}
}
public var swipeActionsStatusLeadingLeft: StatusAction {
didSet {
storage.swipeActionsStatusLeadingLeft = swipeActionsStatusLeadingLeft
}
}
public var swipeActionsStatusLeadingRight: StatusAction {
didSet {
storage.swipeActionsStatusLeadingRight = swipeActionsStatusLeadingRight
}
}
public var swipeActionsUseThemeColor: Bool {
didSet {
storage.swipeActionsUseThemeColor = swipeActionsUseThemeColor
}
}
public var swipeActionsIconStyle: SwipeActionsIconStyle {
didSet {
storage.swipeActionsIconStyle = swipeActionsIconStyle
}
}
public var requestedReview: Bool {
didSet {
storage.requestedReview = requestedReview
}
}
public var collapseLongPosts: Bool {
didSet {
storage.collapseLongPosts = collapseLongPosts
}
}
public var shareButtonBehavior: PreferredShareButtonBehavior {
didSet {
storage.shareButtonBehavior = shareButtonBehavior
}
}
public enum SwipeActionsIconStyle: String, CaseIterable {
case iconWithText, iconOnly
public var description: LocalizedStringKey {
switch self {
case .iconWithText:
@ -246,7 +277,7 @@ import SwiftUI
"enum.swipeactions.icon-only"
}
}
// Have to implement this manually here due to compiler not implicitly
// inserting `nonisolated`, which leads to a warning:
//
@ -257,7 +288,7 @@ import SwiftUI
[.iconWithText, .iconOnly]
}
}
public var postVisibility: Models.Visibility {
if useInstanceContentSettings {
serverPreferences?.postVisibility ?? .pub
@ -265,26 +296,26 @@ import SwiftUI
appDefaultPostVisibility
}
}
public func conformReplyVisibilityConstraints() {
appDefaultReplyVisibility = getReplyVisibility()
}
private func getReplyVisibility() -> Models.Visibility {
getMinVisibility(postVisibility, appDefaultReplyVisibility)
}
public func getReplyVisibility(of status: Status) -> Models.Visibility {
getMinVisibility(getReplyVisibility(), status.visibility)
}
private func getMinVisibility(_ vis1: Models.Visibility, _ vis2: Models.Visibility) -> Models.Visibility {
let no1 = Self.getIntOfVisibility(vis1)
let no2 = Self.getIntOfVisibility(vis2)
return no1 < no2 ? vis1 : vis2
}
public var postIsSensitive: Bool {
if useInstanceContentSettings {
serverPreferences?.postIsSensitive ?? false
@ -292,7 +323,7 @@ import SwiftUI
appDefaultPostsSensitive
}
}
public var autoExpandSpoilers: Bool {
if useInstanceContentSettings {
serverPreferences?.autoExpandSpoilers ?? true
@ -300,7 +331,7 @@ import SwiftUI
appAutoExpandSpoilers
}
}
public var autoExpandMedia: ServerPreferences.AutoExpandMedia {
if useInstanceContentSettings {
serverPreferences?.autoExpandMedia ?? .hideSensitive
@ -308,7 +339,7 @@ import SwiftUI
appAutoExpandMedia
}
}
public var notificationsCount: [OauthToken: Int] = [:] {
didSet {
for (key, value) in notificationsCount {
@ -316,32 +347,32 @@ import SwiftUI
}
}
}
public var totalNotificationsCount: Int {
notificationsCount.compactMap{ $0.value }.reduce(0, +)
notificationsCount.compactMap(\.value).reduce(0, +)
}
public func reloadNotificationsCount(tokens: [OauthToken]) {
notificationsCount = [:]
for token in tokens {
notificationsCount[token] = Self.sharedDefault?.integer(forKey: "push_notifications_count_\(token.createdAt)") ?? 0
}
}
public var serverPreferences: ServerPreferences?
public func setClient(client: Client) {
self.client = client
Task {
await refreshServerPreferences()
}
}
public func refreshServerPreferences() async {
guard let client, client.isAuth else { return }
serverPreferences = try? await client.get(endpoint: Accounts.preferences)
}
public func markLanguageAsSelected(isoCode: String) {
var copy = recentlyUsedLanguages
if let index = copy.firstIndex(of: isoCode) {
@ -350,7 +381,7 @@ import SwiftUI
copy.insert(isoCode, at: 0)
recentlyUsedLanguages = Array(copy.prefix(3))
}
public static func getIntOfVisibility(_ vis: Models.Visibility) -> Int {
switch vis {
case .direct:
@ -363,7 +394,7 @@ import SwiftUI
3
}
}
private init() {
preferredBrowser = storage.preferredBrowser
showTranslateButton = storage.showTranslateButton

View file

@ -7,11 +7,11 @@ public struct CardsListView: View {
@Environment(Theme.self) private var theme
let cards: [Card]
public init(cards: [Card]) {
self.cards = cards
}
public var body: some View {
List {
ForEach(cards) { card in

View file

@ -226,7 +226,7 @@ public struct ExploreView: View {
.listRowBackground(theme.primaryBackgroundColor)
.padding(.vertical, 8)
}
NavigationLink(value: RouterDestination.trendingLinks(cards: viewModel.trendingLinks)) {
Text("see-more")
.foregroundColor(theme.tintColor)

View file

@ -1,15 +1,15 @@
import Foundation
import SwiftData
import SwiftUI
import Foundation
@Model public class Draft {
@Attribute(.unique) public var id: UUID
public var content: String
public var creationDate: Date
public init(content: String) {
self.id = UUID()
id = UUID()
self.content = content
self.creationDate = Date()
creationDate = Date()
}
}

View file

@ -1,13 +1,13 @@
import Foundation
import SwiftData
import SwiftUI
import Foundation
@Model public class LocalTimeline {
public var instance: String
public var creationDate: Date
public init(instance: String) {
self.instance = instance
self.creationDate = Date()
creationDate = Date()
}
}

View file

@ -1,18 +1,18 @@
import Foundation
import SwiftData
import SwiftUI
import Foundation
@Model public class TagGroup: Equatable {
public var title: String
public var symbolName: String
public var tags: [String]
public var creationDate: Date
public init(title: String, symbolName: String, tags: [String]) {
self.title = title
self.symbolName = symbolName
self.tags = tags
self.creationDate = Date()
creationDate = Date()
}
}

View file

@ -62,4 +62,3 @@ public struct FeaturedTag: Codable, Identifiable {
extension Tag: Sendable {}
extension Tag.History: Sendable {}
extension FeaturedTag: Sendable {}

View file

@ -177,7 +177,7 @@ struct StatusEditorAccessoryView: View {
viewModel.setInitialLanguageSelection(preference: preferences.recentlyUsedLanguages.first ?? preferences.serverPreferences?.postLanguage)
}
}
private var draftsListView: some View {
DraftsListView(selectedDraft: .init(get: {
nil

View file

@ -1,20 +1,20 @@
import SwiftUI
import SwiftData
import DesignSystem
import Models
import SwiftData
import SwiftUI
struct DraftsListView: View {
@AppStorage("draft_posts") public var legacyDraftPosts: [String] = []
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var context
@Environment(Theme.self) private var theme
@Query(sort: \Draft.creationDate, order: .reverse) var drafts: [Draft]
@Binding var selectedDraft: Draft?
var body: some View {
NavigationStack {
List {
@ -54,7 +54,7 @@ struct DraftsListView: View {
}
}
}
func migrateUserPreferencesDraft() {
for draft in legacyDraftPosts {
let newDraft = Draft(content: draft)

View file

@ -161,7 +161,8 @@ public struct StatusEditorView: View {
object: nil)
}
Button("action.cancel", role: .cancel) {}
})
}
)
}
}
}

View file

@ -33,7 +33,8 @@ public struct TimelineView: View {
public init(timeline: Binding<TimelineFilter>,
selectedTagGroup: Binding<TagGroup?>,
scrollToTopSignal: Binding<Int>, canFilterTimeline: Bool) {
scrollToTopSignal: Binding<Int>, canFilterTimeline: Bool)
{
_timeline = timeline
_selectedTagGroup = selectedTagGroup
_scrollToTopSignal = scrollToTopSignal