diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index f6d49d6..99b5596 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -345,6 +345,9 @@ } } }, + "Вы выйдете из выбранной сессии." : { + "comment" : "Описание подтверждения завершения конкретной сессии" + }, "Вы выйдете со всех устройств, кроме текущего." : { "comment" : "Описание подтверждения завершения сессий" }, @@ -458,7 +461,7 @@ }, "Завершить" : { - "comment" : "Подтверждение завершения других сессий" + "comment" : "Кнопка завершения конкретной сессии\nПодтверждение завершения других сессий\nПодтверждение завершения конкретной сессии" }, "Завершить другие сессии" : { "comment" : "Кнопка завершения других сессий" @@ -466,6 +469,9 @@ "Завершить сессии на других устройствах?" : { "comment" : "Заголовок подтверждения завершения сессий" }, + "Завершить эту сессию?" : { + "comment" : "Заголовок подтверждения завершения отдельной сессии" + }, "Заглушка: Push-уведомления" : { }, diff --git a/yobble/Views/Tab/Settings/ActiveSessionsView.swift b/yobble/Views/Tab/Settings/ActiveSessionsView.swift index 741f6af..2867887 100644 --- a/yobble/Views/Tab/Settings/ActiveSessionsView.swift +++ b/yobble/Views/Tab/Settings/ActiveSessionsView.swift @@ -7,6 +7,8 @@ struct ActiveSessionsView: View { @State private var revokeInProgress = false @State private var activeAlert: SessionsAlert? @State private var showRevokeConfirmation = false + @State private var sessionPendingRevoke: SessionViewData? + @State private var revokingSessionIds: Set = [] private let sessionsService = SessionsService() private var currentSession: SessionViewData? { @@ -60,7 +62,18 @@ struct ActiveSessionsView: View { if !otherSessions.isEmpty { Section(header: Text(String(format: NSLocalizedString("Другие устройства (%d)", comment: "Заголовок секции других устройств с количеством"), otherSessions.count))) { ForEach(otherSessions) { session in - sessionRow(for: session) + let isRevoking = isRevoking(session: session) + + sessionRow(for: session, isRevoking: isRevoking) + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .destructive) { + sessionPendingRevoke = session + } label: { + Label(NSLocalizedString("Завершить", comment: "Кнопка завершения конкретной сессии"), systemImage: "trash") + } + .disabled(isRevoking) + } + .disabled(isRevoking) } } } @@ -74,6 +87,24 @@ struct ActiveSessionsView: View { .refreshable { await loadSessions(force: true) } + .confirmationDialog( + NSLocalizedString("Завершить эту сессию?", comment: "Заголовок подтверждения завершения отдельной сессии"), + isPresented: Binding( + get: { sessionPendingRevoke != nil }, + set: { if !$0 { sessionPendingRevoke = nil } } + ), + presenting: sessionPendingRevoke + ) { session in + Button(NSLocalizedString("Завершить", comment: "Подтверждение завершения конкретной сессии"), role: .destructive) { + sessionPendingRevoke = nil + Task { await revoke(session: session) } + } + Button(NSLocalizedString("Отмена", comment: "Общий текст кнопки отмены"), role: .cancel) { + sessionPendingRevoke = nil + } + } message: { _ in + Text(NSLocalizedString("Вы выйдете из выбранной сессии.", comment: "Описание подтверждения завершения конкретной сессии")) + } .alert(item: $activeAlert) { alert in Alert( title: Text(alert.title), @@ -136,7 +167,7 @@ struct ActiveSessionsView: View { .listRowSeparator(.hidden) } - private func sessionRow(for session: SessionViewData) -> some View { + private func sessionRow(for session: SessionViewData, isRevoking: Bool = false) -> some View { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 6) { @@ -157,6 +188,9 @@ struct ActiveSessionsView: View { .background(Color.accentColor.opacity(0.15)) .foregroundColor(.accentColor) .clipShape(Capsule()) + } else if isRevoking { + ProgressView() + .progressViewStyle(.circular) } } @@ -225,6 +259,34 @@ struct ActiveSessionsView: View { } } + @MainActor + private func revoke(session: SessionViewData) async { + guard !session.isCurrent, !isRevoking(session: session) else { + return + } + + revokingSessionIds.insert(session.id) + defer { revokingSessionIds.remove(session.id) } + + do { + let message = try await sessionsService.revoke(sessionId: session.id) + sessions.removeAll { $0.id == session.id } + activeAlert = SessionsAlert( + title: NSLocalizedString("Готово", comment: "Заголовок успешного уведомления"), + message: message + ) + } catch { + activeAlert = SessionsAlert( + title: NSLocalizedString("Ошибка", comment: "Заголовок сообщения об ошибке"), + message: error.localizedDescription + ) + } + } + + private func isRevoking(session: SessionViewData) -> Bool { + revokingSessionIds.contains(session.id) + } + private var revokeOtherSessionsButton: some View { let primaryColor: Color = revokeInProgress ? .secondary : .red