Merge remote-tracking branch 'origin/main' into HEAD
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"project": "apple-mail-new",
|
||||
"locale": "5f46109dcb770a428c3cb55dbbed93e75be1363f"
|
||||
"locale": "3d23586db025238c56593e2c1bae1bff693337fb"
|
||||
}
|
||||
@@ -19,5 +19,5 @@ import InboxCore
|
||||
import proton_app_uniffi
|
||||
|
||||
extension ApiConfig {
|
||||
static let current = Self.init(userAgent: UserAgent.value(appName: "ProtonMail"), envId: .current)
|
||||
static let current = Self.init(envId: .current)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ protocol SenderImageDataSource {
|
||||
}
|
||||
|
||||
/// This is the information required by the Rust SDK to retrieve a sender's avatar image
|
||||
struct SenderImageDataParameters: Equatable {
|
||||
struct SenderImageDataParameters: Hashable {
|
||||
let address: String
|
||||
let bimiSelector: String?
|
||||
let displaySenderImage: Bool
|
||||
|
||||
@@ -20,11 +20,14 @@ import InboxDesignSystem
|
||||
|
||||
enum InternalAction {
|
||||
case more
|
||||
case editToolbar
|
||||
|
||||
var icon: Image {
|
||||
switch self {
|
||||
case .more:
|
||||
DS.Icon.icThreeDotsHorizontal.image
|
||||
case .editToolbar:
|
||||
DS.Icon.icMagicWand.image
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +35,8 @@ enum InternalAction {
|
||||
switch self {
|
||||
case .more:
|
||||
.empty
|
||||
case .editToolbar:
|
||||
L10n.Action.editToolbar
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,4 +47,12 @@ extension InternalAction: DisplayableAction {
|
||||
.init(title: name, image: icon)
|
||||
}
|
||||
|
||||
var isMoreAction: Bool {
|
||||
if case .more = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,14 +29,8 @@ struct NotificationAuthorizationService: ApplicationServiceSetUp {
|
||||
self.remoteNotificationRegistrar = remoteNotificationRegistrar
|
||||
}
|
||||
|
||||
func setUpServiceAsync() async {
|
||||
await remoteNotificationRegistrar.registerForRemoteNotifications()
|
||||
}
|
||||
|
||||
func setUpService() {
|
||||
Task {
|
||||
await setUpServiceAsync()
|
||||
}
|
||||
remoteNotificationRegistrar.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app-icon-calculator.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app-icon-notes.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 317 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app-icon-weather.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 322 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app-icon-disguised-calculator-29@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-icon-disguised-calculator-29@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-icon-disguised-calculator-29@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 860 B |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app-icon-disguised-notes-29@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-icon-disguised-notes-29@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-icon-disguised-notes-29@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app-icon-preview29@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-icon-preview29@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-icon-preview29@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 569 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app-icon-disguised-weather-29@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-icon-disguised-weather-29@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-icon-disguised-weather-29@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
@@ -968,7 +968,7 @@
|
||||
"be" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Патрабуецца для захавання файлаў у дадатку «Фота»."
|
||||
"value" : "Патрабуецца для захавання файлаў у праграме «Фатаграфіі»."
|
||||
}
|
||||
},
|
||||
"ca" : {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import UIKit
|
||||
import InboxDesignSystem
|
||||
|
||||
enum MailShortcutItem: String, CaseIterable {
|
||||
case search
|
||||
|
||||
@@ -183,12 +183,22 @@ extension AppContext {
|
||||
}
|
||||
AppLogger.log(message: "initializeUserSession finished", category: .userSessions)
|
||||
} catch {
|
||||
AppLogger.log(error: error, category: .userSessions)
|
||||
errorSubject.send(error)
|
||||
logAndDisplayError(error)
|
||||
|
||||
do {
|
||||
try await mailSession.deleteAccount(userId: session.userId()).get()
|
||||
} catch {
|
||||
logAndDisplayError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func logAndDisplayError(_ error: Error) {
|
||||
AppLogger.log(error: error, category: .userSessions)
|
||||
errorSubject.send(error)
|
||||
}
|
||||
|
||||
private func initializeUserSession(session: StoredSession) async throws -> MailUserSession? {
|
||||
AppLogger.log(message: "Creating a new session", category: .userSessions)
|
||||
|
||||
@@ -209,7 +219,6 @@ extension AppContext {
|
||||
let earliestNextAttemptTime = start + minimumTimeBetweenRetries
|
||||
try await Task.sleep(until: earliestNextAttemptTime)
|
||||
case .error(let error):
|
||||
try await mailSession.deleteAccount(userId: session.userId()).get()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,9 +508,11 @@ extension MailboxModel {
|
||||
|
||||
func createDraft() {
|
||||
Task {
|
||||
await draftPresenter.openNewDraft(onError: {
|
||||
toast = .error(message: $0.localizedDescription)
|
||||
})
|
||||
do {
|
||||
try await draftPresenter.openNewDraft()
|
||||
} catch {
|
||||
toast = .error(message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import proton_app_uniffi
|
||||
import InboxDesignSystem
|
||||
|
||||
extension ConversationAction: DisplayableAction {
|
||||
|
||||
var displayData: ActionDisplayData {
|
||||
switch self {
|
||||
case .labelAs:
|
||||
@@ -54,4 +54,12 @@ extension ConversationAction: DisplayableAction {
|
||||
InternalAction.more.displayData
|
||||
}
|
||||
}
|
||||
|
||||
var isMoreAction: Bool {
|
||||
if case .more = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,67 +21,66 @@ import InboxCore
|
||||
import InboxCoreUI
|
||||
import SwiftUI
|
||||
|
||||
struct ConversationActionsSheet: View {
|
||||
struct ConversationActionsMenu<OpenMenuButtonContent: View>: View {
|
||||
private let conversationID: ID
|
||||
private let title: String
|
||||
private let mailbox: Mailbox
|
||||
private let mailUserSession: MailUserSession
|
||||
private let actionTapped: (ConversationAction) -> Void
|
||||
private let editToolbarTapped: () -> Void
|
||||
private let label: () -> OpenMenuButtonContent
|
||||
|
||||
@State var actions: ConversationActionSheet?
|
||||
@State var isEditToolbarPresented = false
|
||||
|
||||
init(
|
||||
conversationID: ID,
|
||||
title: String,
|
||||
mailbox: Mailbox,
|
||||
mailUserSession: MailUserSession,
|
||||
actionTapped: @escaping (ConversationAction) -> Void
|
||||
actionTapped: @escaping (ConversationAction) -> Void,
|
||||
editToolbarTapped: @escaping () -> Void,
|
||||
label: @escaping () -> OpenMenuButtonContent
|
||||
) {
|
||||
self.conversationID = conversationID
|
||||
self.title = title
|
||||
self.mailbox = mailbox
|
||||
self.mailUserSession = mailUserSession
|
||||
self.actionTapped = actionTapped
|
||||
self.editToolbarTapped = editToolbarTapped
|
||||
self.label = label
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ClosableScreen {
|
||||
ScrollView {
|
||||
VStack(spacing: DS.Spacing.standard) {
|
||||
if let actions {
|
||||
verticalSection(actions: actions.conversationActions)
|
||||
verticalSection(actions: actions.moveActions)
|
||||
Menu {
|
||||
Group {
|
||||
if let actions {
|
||||
Section {
|
||||
ActionMenuButton(displayData: InternalAction.editToolbar.displayData, action: editToolbarTapped)
|
||||
}
|
||||
verticalSection(actions: actions.moveActions)
|
||||
verticalSection(actions: actions.conversationActions)
|
||||
|
||||
EditToolbarSheetSection {
|
||||
Task { await handle(action: .editToolbarTapped) }
|
||||
}
|
||||
} else {
|
||||
Text(String.empty.notLocalized)
|
||||
}
|
||||
.padding(.all, DS.Spacing.large)
|
||||
}
|
||||
.background(DS.Color.BackgroundInverted.norm)
|
||||
.navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.onLoad { Task { await handle(action: .onLoad) } }
|
||||
.sheet(isPresented: $isEditToolbarPresented) {
|
||||
EditToolbarScreen(
|
||||
state: .initial(toolbarType: .conversation),
|
||||
customizeToolbarService: mailUserSession
|
||||
)
|
||||
.onLoad { Task { await handle(action: .onLoad) } }
|
||||
} label: {
|
||||
label()
|
||||
}
|
||||
}
|
||||
|
||||
private func verticalSection(actions: [ConversationAction]) -> some View {
|
||||
ActionSheetVerticalSection(actions: actions) { action in
|
||||
Task {
|
||||
await handle(action: .actionTapped(action))
|
||||
Section {
|
||||
ForEach(actions, id: \.self) { action in
|
||||
ActionMenuButton(displayData: action.displayData) {
|
||||
Task {
|
||||
await handle(action: .actionTapped(action))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handle(action: ConversationActionsSheetAction) async {
|
||||
private func handle(action: ConversationActionsMenuAction) async {
|
||||
switch action {
|
||||
case .onLoad:
|
||||
await loadActions()
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
import proton_app_uniffi
|
||||
|
||||
enum ConversationActionsSheetAction {
|
||||
enum ConversationActionsMenuAction {
|
||||
case onLoad
|
||||
case actionTapped(ConversationAction)
|
||||
case editToolbarTapped
|
||||
@@ -33,11 +33,4 @@ extension DeleteActions {
|
||||
)
|
||||
}
|
||||
|
||||
static var dummy: Self {
|
||||
.init(
|
||||
message: { _, _ in .ok },
|
||||
conversation: { _, _ in .ok }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ struct DraftPresenter: ContactsDraftPresenter {
|
||||
self.undoScheduleSendProvider = undoScheduleSendProvider
|
||||
}
|
||||
|
||||
func openNewDraft(onError: (DraftOpenError) -> Void) async {
|
||||
func openNewDraft() async throws(DraftOpenError) {
|
||||
AppLogger.log(message: "open new draft", category: .composer)
|
||||
await openNewDraft(createMode: .empty, onError: onError)
|
||||
try await openNewDraft(createMode: .empty, updateDraft: .none)
|
||||
}
|
||||
|
||||
func openDraft(withId messageId: ID, lastScheduledTime: UInt64? = nil) {
|
||||
@@ -54,7 +54,7 @@ struct DraftPresenter: ContactsDraftPresenter {
|
||||
draftToPresentSubject.send(.openDraftId(messageId: messageId, lastScheduledTime: lastScheduledTime))
|
||||
}
|
||||
|
||||
func openDraft(with recipient: SingleRecipientEntry) async throws {
|
||||
func openDraft(with recipient: SingleRecipientEntry) async throws(DraftOpenError) {
|
||||
AppLogger.log(message: "open new draft with single recipient", category: .composer)
|
||||
|
||||
try await openNewEmptyDraft { toRecipients in
|
||||
@@ -62,7 +62,7 @@ struct DraftPresenter: ContactsDraftPresenter {
|
||||
}
|
||||
}
|
||||
|
||||
func openDraft(with group: ContactGroupItem) async throws {
|
||||
func openDraft(with group: ContactGroupItem) async throws(DraftOpenError) {
|
||||
AppLogger.log(message: "open new draft with contact group details", category: .composer)
|
||||
|
||||
try await openNewEmptyDraft { toRecipients in
|
||||
@@ -125,15 +125,9 @@ struct DraftPresenter: ContactsDraftPresenter {
|
||||
try await openNewDraft(createMode: .fromIosShareExtension, updateDraft: .none)
|
||||
}
|
||||
|
||||
func handleReplyAction(for messageId: ID, action: ReplyAction, onError: (DraftOpenError) -> Void) async {
|
||||
switch action {
|
||||
case .reply:
|
||||
await openReplyDraft(for: messageId, onError: onError)
|
||||
case .replyAll:
|
||||
await openReplyAllDraft(for: messageId, onError: onError)
|
||||
case .forward:
|
||||
await openForwardDraft(for: messageId, onError: onError)
|
||||
}
|
||||
func handleReplyAction(for messageId: ID, action: ReplyAction) async throws(DraftOpenError) {
|
||||
AppLogger.log(message: action.logDescription, category: .composer)
|
||||
try await openNewDraft(createMode: action.createMode(messageId: messageId), updateDraft: .none)
|
||||
}
|
||||
|
||||
func undoSentMessageAndOpenDraft(for messageId: ID) async throws(DraftUndoSendError) {
|
||||
@@ -152,32 +146,6 @@ struct DraftPresenter: ContactsDraftPresenter {
|
||||
|
||||
extension DraftPresenter {
|
||||
|
||||
private func openReplyDraft(for messageId: ID, onError: (DraftOpenError) -> Void) async {
|
||||
AppLogger.log(message: "open reply draft", category: .composer)
|
||||
await openNewDraft(createMode: .reply(messageId), onError: onError)
|
||||
}
|
||||
|
||||
private func openReplyAllDraft(for messageId: ID, onError: (DraftOpenError) -> Void) async {
|
||||
AppLogger.log(message: "open reply all draft", category: .composer)
|
||||
await openNewDraft(createMode: .replyAll(messageId), onError: onError)
|
||||
}
|
||||
|
||||
private func openForwardDraft(for messageId: ID, onError: (DraftOpenError) -> Void) async {
|
||||
AppLogger.log(message: "open forward draft", category: .composer)
|
||||
await openNewDraft(createMode: .forward(messageId), onError: onError)
|
||||
}
|
||||
|
||||
private func openNewDraft(
|
||||
createMode: DraftCreateMode,
|
||||
onError: (DraftOpenError) -> Void
|
||||
) async {
|
||||
do {
|
||||
try await openNewDraft(createMode: createMode, updateDraft: .none)
|
||||
} catch {
|
||||
onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
private func openNewDraft(
|
||||
createMode: DraftCreateMode,
|
||||
updateDraft: ((Draft) -> Void)?
|
||||
@@ -192,7 +160,7 @@ extension DraftPresenter {
|
||||
}
|
||||
}
|
||||
|
||||
private func openNewEmptyDraft(updateToRecipients: @escaping (ComposerRecipientList) -> Void) async throws {
|
||||
private func openNewEmptyDraft(updateToRecipients: @escaping (ComposerRecipientList) -> Void) async throws(DraftOpenError) {
|
||||
let updateDraft: (Draft) -> Void = { draft in
|
||||
updateToRecipients(draft.toRecipients())
|
||||
}
|
||||
@@ -202,6 +170,24 @@ extension DraftPresenter {
|
||||
|
||||
}
|
||||
|
||||
private extension ReplyAction {
|
||||
var logDescription: String {
|
||||
switch self {
|
||||
case .reply: "open reply draft"
|
||||
case .replyAll: "open reply all draft"
|
||||
case .forward: "open forward draft"
|
||||
}
|
||||
}
|
||||
|
||||
func createMode(messageId: ID) -> DraftCreateMode {
|
||||
switch self {
|
||||
case .reply: .reply(messageId)
|
||||
case .replyAll: .replyAll(messageId)
|
||||
case .forward: .forward(messageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DraftPresenter {
|
||||
|
||||
static func dummy(
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import InboxDesignSystem
|
||||
import proton_app_uniffi
|
||||
|
||||
extension MessageAction: DisplayableAction {
|
||||
@@ -72,4 +71,12 @@ extension MessageAction: DisplayableAction {
|
||||
}
|
||||
}
|
||||
|
||||
var isMoreAction: Bool {
|
||||
if case .more = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,23 +21,27 @@ import InboxCoreUI
|
||||
import InboxDesignSystem
|
||||
import SwiftUI
|
||||
|
||||
struct MessageActionsSheet: View {
|
||||
private let state: MessageActionsSheetState
|
||||
struct MessageActionsMenu<OpenMenuButtonContent: View>: View {
|
||||
private let state: MessageActionsMenuState
|
||||
private let mailbox: Mailbox
|
||||
private let mailUserSession: MailUserSession
|
||||
private let service: AllAvailableMessageActionsForActionSheetService
|
||||
private let messageAppearanceOverrideStore: MessageAppearanceOverrideStore
|
||||
private let actionTapped: (MessageAction) -> Void
|
||||
private let editToolbarTapped: () -> Void
|
||||
private let label: () -> OpenMenuButtonContent
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(
|
||||
state: MessageActionsSheetState,
|
||||
state: MessageActionsMenuState,
|
||||
mailbox: Mailbox,
|
||||
mailUserSession: MailUserSession,
|
||||
messageAppearanceOverrideStore: MessageAppearanceOverrideStore,
|
||||
service: @escaping AllAvailableMessageActionsForActionSheetService = allAvailableMessageActionsForActionSheet,
|
||||
actionTapped: @escaping (MessageAction) -> Void,
|
||||
editToolbarTapped: @escaping () -> Void,
|
||||
label: @escaping () -> OpenMenuButtonContent
|
||||
) {
|
||||
self.state = state
|
||||
self.mailbox = mailbox
|
||||
@@ -45,11 +49,13 @@ struct MessageActionsSheet: View {
|
||||
self.service = service
|
||||
self.actionTapped = actionTapped
|
||||
self.messageAppearanceOverrideStore = messageAppearanceOverrideStore
|
||||
self.editToolbarTapped = editToolbarTapped
|
||||
self.label = label
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
StoreView(
|
||||
store: MessageActionsSheetStore(
|
||||
store: MessageActionsMenuStore(
|
||||
state: state,
|
||||
mailbox: mailbox,
|
||||
messageAppearanceOverrideStore: messageAppearanceOverrideStore,
|
||||
@@ -57,68 +63,71 @@ struct MessageActionsSheet: View {
|
||||
actionTapped: actionTapped
|
||||
)
|
||||
) { state, store in
|
||||
ClosableScreen {
|
||||
ScrollView {
|
||||
VStack(spacing: DS.Spacing.standard) {
|
||||
Menu {
|
||||
Group {
|
||||
if store.state.actions.isEmpty {
|
||||
// Workaround to ensure the menu is displayed after dynamic actions are loaded.
|
||||
// Without this, the menu will not be presented.
|
||||
Text(String.empty.notLocalized)
|
||||
} else {
|
||||
horizontalSection(actions: store.state.actions.replyActions, store: store)
|
||||
verticalSection(actions: store.state.actions.messageActions, store: store)
|
||||
verticalSection(actions: store.state.actions.moveActions, store: store)
|
||||
verticalSection(actions: store.state.actions.generalActions, store: store)
|
||||
|
||||
if state.isEditToolbarVisible {
|
||||
EditToolbarSheetSection {
|
||||
store.handle(action: .editToolbarTapped)
|
||||
Menu {
|
||||
verticalSection(actions: store.state.actions.generalActions, store: store)
|
||||
if state.showEditToolbar {
|
||||
Section {
|
||||
ActionMenuButton(
|
||||
displayData: InternalAction.editToolbar.displayData,
|
||||
action: editToolbarTapped
|
||||
)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(L10n.Action.moreOptions)
|
||||
}
|
||||
}
|
||||
.padding(.all, DS.Spacing.large)
|
||||
}
|
||||
.background(DS.Color.BackgroundInverted.norm)
|
||||
.navigationTitle(store.state.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.onLoad {
|
||||
store.handle(action: .colorSchemeChanged(colorScheme))
|
||||
store.handle(action: .onLoad)
|
||||
}
|
||||
.onChange(of: colorScheme) { _, newValue in
|
||||
store.handle(action: .colorSchemeChanged(newValue))
|
||||
}
|
||||
.sheet(isPresented: store.binding(\.isEditToolbarPresented)) {
|
||||
EditToolbarScreen(state: .initial(toolbarType: .message), customizeToolbarService: mailUserSession)
|
||||
.onLoad {
|
||||
store.handle(action: .colorSchemeChanged(colorScheme))
|
||||
store.handle(action: .onLoad)
|
||||
}
|
||||
.onChange(of: colorScheme) { _, newValue in
|
||||
store.handle(action: .colorSchemeChanged(newValue))
|
||||
}
|
||||
} label: {
|
||||
label()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func horizontalSection(actions: [MessageAction], store: MessageActionsSheetStore) -> some View {
|
||||
HStack(spacing: DS.Spacing.standard) {
|
||||
@ViewBuilder
|
||||
private func horizontalSection(actions: [MessageAction], store: MessageActionsMenuStore) -> some View {
|
||||
ControlGroup {
|
||||
ForEach(actions, id: \.self) { action in
|
||||
horizontalButton(action: action, store: store)
|
||||
ActionMenuButton(displayData: action.displayData) {
|
||||
store.handle(action: .actionTapped(action))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func verticalSection(actions: [MessageAction], store: MessageActionsSheetStore) -> some View {
|
||||
ActionSheetVerticalSection(actions: actions) { action in
|
||||
store.handle(action: .actionTapped(action))
|
||||
}
|
||||
}
|
||||
|
||||
private func horizontalButton(action: MessageAction, store: MessageActionsSheetStore) -> some View {
|
||||
Button(action: { store.handle(action: .actionTapped(action)) }) {
|
||||
VStack(spacing: DS.Spacing.standard) {
|
||||
action.displayData.image
|
||||
.square(size: 24)
|
||||
.foregroundStyle(DS.Color.Icon.norm)
|
||||
Text(action.displayData.title)
|
||||
.font(.body)
|
||||
.foregroundStyle(DS.Color.Text.norm)
|
||||
@ViewBuilder
|
||||
private func verticalSection(actions: [MessageAction], store: MessageActionsMenuStore) -> some View {
|
||||
Section {
|
||||
ForEach(actions, id: \.self) { action in
|
||||
ActionMenuButton(displayData: action.displayData) {
|
||||
store.handle(action: .actionTapped(action))
|
||||
}
|
||||
}
|
||||
.frame(height: 84)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(RegularButtonStyle())
|
||||
.background(DS.Color.BackgroundInverted.secondary)
|
||||
.clipShape(.rect(cornerRadius: DS.Radius.extraLarge))
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageActionSheet {
|
||||
|
||||
var isEmpty: Bool {
|
||||
replyActions.isEmpty && generalActions.isEmpty && messageActions.isEmpty && moveActions.isEmpty
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,9 +18,8 @@
|
||||
import proton_app_uniffi
|
||||
import SwiftUI
|
||||
|
||||
enum MessageActionsSheetAction {
|
||||
enum MessageActionsMenuAction {
|
||||
case onLoad
|
||||
case actionTapped(MessageAction)
|
||||
case colorSchemeChanged(ColorScheme)
|
||||
case editToolbarTapped
|
||||
}
|
||||
@@ -19,30 +19,26 @@ import proton_app_uniffi
|
||||
import InboxCore
|
||||
import SwiftUI
|
||||
|
||||
struct MessageActionsSheetState: Copying, Equatable {
|
||||
struct MessageActionsMenuState: Copying, Equatable {
|
||||
let messageID: ID
|
||||
let title: String
|
||||
let isEditToolbarVisible: Bool
|
||||
let showEditToolbar: Bool
|
||||
var actions: MessageActionSheet
|
||||
var colorScheme: ColorScheme
|
||||
var isEditToolbarPresented: Bool
|
||||
}
|
||||
|
||||
extension MessageActionsSheetState {
|
||||
extension MessageActionsMenuState {
|
||||
|
||||
static func initial(messageID: ID, title: String, isEditToolbarVisible: Bool) -> Self {
|
||||
static func initial(messageID: ID, showEditToolbar: Bool) -> Self {
|
||||
.init(
|
||||
messageID: messageID,
|
||||
title: title,
|
||||
isEditToolbarVisible: isEditToolbarVisible,
|
||||
showEditToolbar: showEditToolbar,
|
||||
actions: .init(
|
||||
replyActions: [],
|
||||
messageActions: [],
|
||||
moveActions: [],
|
||||
generalActions: []
|
||||
),
|
||||
colorScheme: .light,
|
||||
isEditToolbarPresented: false
|
||||
colorScheme: .light
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import InboxCoreUI
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
class MessageActionsSheetStore: StateStore {
|
||||
@Published var state: MessageActionsSheetState
|
||||
class MessageActionsMenuStore: StateStore {
|
||||
@Published var state: MessageActionsMenuState
|
||||
|
||||
private let mailbox: Mailbox
|
||||
private let service: AllAvailableMessageActionsForActionSheetService
|
||||
@@ -30,7 +30,7 @@ class MessageActionsSheetStore: StateStore {
|
||||
private let messageAppearanceOverrideStore: MessageAppearanceOverrideStore
|
||||
|
||||
init(
|
||||
state: MessageActionsSheetState,
|
||||
state: MessageActionsMenuState,
|
||||
mailbox: Mailbox,
|
||||
messageAppearanceOverrideStore: MessageAppearanceOverrideStore,
|
||||
service: @escaping AllAvailableMessageActionsForActionSheetService,
|
||||
@@ -43,7 +43,7 @@ class MessageActionsSheetStore: StateStore {
|
||||
self.actionTapped = actionTapped
|
||||
}
|
||||
|
||||
func handle(action: MessageActionsSheetAction) async {
|
||||
func handle(action: MessageActionsMenuAction) async {
|
||||
switch action {
|
||||
case .onLoad:
|
||||
await loadActions()
|
||||
@@ -51,8 +51,6 @@ class MessageActionsSheetStore: StateStore {
|
||||
actionTapped(action)
|
||||
case .colorSchemeChanged(let colorScheme):
|
||||
state = state.copy(\.colorScheme, to: colorScheme)
|
||||
case .editToolbarTapped:
|
||||
state = state.copy(\.isEditToolbarPresented, to: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
protocol DisplayableAction {
|
||||
protocol DisplayableAction: Hashable {
|
||||
var displayData: ActionDisplayData { get }
|
||||
var isMoreAction: Bool { get }
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import proton_app_uniffi
|
||||
import WebKit
|
||||
|
||||
@MainActor
|
||||
final class MessagePrinter: PrintActionPerformer {
|
||||
final class MessagePrinter {
|
||||
typealias FindMessage = (ID) async throws -> Message?
|
||||
typealias PresentPrintInteractionController = (WebViewPrintingTransaction) async throws(PrintError) -> Void
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ final class WebViewPrintingTransaction {
|
||||
self.webView = webView
|
||||
}
|
||||
|
||||
func perform<Output>(block: (Message, WKWebView) async throws(PrintError) -> Output) async throws(PrintError) -> Output {
|
||||
func perform<Output: Sendable>(block: (Message, WKWebView) async throws(PrintError) -> Output) async throws(PrintError) -> Output {
|
||||
let headerImage = renderHeaderImage(
|
||||
subject: message.subject,
|
||||
messageDetails: message.toExpandedMessageCellUIModel().messageDetails
|
||||
|
||||
@@ -38,13 +38,4 @@ extension ReadActionPerformerActions {
|
||||
)
|
||||
}
|
||||
|
||||
static var dummy: Self {
|
||||
.init(
|
||||
markMessageAsRead: { _, _ in .ok },
|
||||
markConversationAsRead: { _, _ in .ok },
|
||||
markMessageAsUnread: { _, _ in .ok },
|
||||
markConversationAsUnread: { _, _ in .ok }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import InboxIAP
|
||||
import SwiftUI
|
||||
import proton_app_uniffi
|
||||
|
||||
@MainActor
|
||||
class SnoozeStore: StateStore {
|
||||
@Published var state: SnoozeState
|
||||
private let upsellScreenPresenter: UpsellScreenPresenter
|
||||
|
||||
@@ -38,13 +38,4 @@ extension StarActionPerformerActions {
|
||||
)
|
||||
}
|
||||
|
||||
static var dummy: StarActionPerformerActions {
|
||||
.init(
|
||||
starMessage: { _, _ in .ok },
|
||||
starConversation: { _, _ in .ok },
|
||||
unstarMessage: { _, _ in .ok },
|
||||
unstarConversation: { _, _ in .ok }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,27 +15,30 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import InboxCoreUI
|
||||
import InboxDesignSystem
|
||||
import SwiftUI
|
||||
|
||||
struct ActionSheetVerticalSection<Action: DisplayableAction>: View {
|
||||
private let actions: [Action]
|
||||
private let actionSelected: (Action) -> Void
|
||||
struct ActionMenuButton: View {
|
||||
private let displayData: ActionDisplayData
|
||||
private let action: () -> Void
|
||||
|
||||
init(actions: [Action], actionSelected: @escaping (Action) -> Void) {
|
||||
self.actions = actions
|
||||
self.actionSelected = actionSelected
|
||||
init(displayData: ActionDisplayData, action: @escaping () -> Void) {
|
||||
self.displayData = displayData
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ActionSheetSection {
|
||||
ForEachLast(collection: actions) { action, isLast in
|
||||
ActionSheetImageButton(
|
||||
displayData: action.displayData,
|
||||
displayBottomSeparator: !isLast
|
||||
) {
|
||||
actionSelected(action)
|
||||
}
|
||||
Button {
|
||||
action()
|
||||
} label: {
|
||||
Label {
|
||||
Text(displayData.title)
|
||||
.font(.body)
|
||||
.foregroundStyle(DS.Color.Text.norm)
|
||||
} icon: {
|
||||
displayData.image
|
||||
.square(size: 24)
|
||||
.foregroundStyle(DS.Color.Icon.norm)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import SwiftUI
|
||||
|
||||
extension View {
|
||||
func listActionsToolbar(
|
||||
state: ListActionsToolbarState,
|
||||
initialState: ListActionsToolbarState,
|
||||
availableActions: AvailableListToolbarActions,
|
||||
itemTypeForActionBar: MailboxItemType,
|
||||
mailUserSession: MailUserSession,
|
||||
@@ -30,7 +30,7 @@ extension View {
|
||||
) -> some View {
|
||||
modifier(
|
||||
ListActionBarViewModifier(
|
||||
state: state,
|
||||
initialState: initialState,
|
||||
availableActions: availableActions,
|
||||
itemTypeForActionBar: itemTypeForActionBar,
|
||||
mailUserSession: mailUserSession,
|
||||
@@ -40,11 +40,14 @@ extension View {
|
||||
}
|
||||
|
||||
private struct ListActionBarViewModifier: ViewModifier {
|
||||
typealias State = ListActionsToolbarState
|
||||
typealias Store = ListActionsToolbarStore
|
||||
|
||||
@Binding var selectedItems: Set<MailboxSelectedItem>
|
||||
@EnvironmentObject var mailbox: Mailbox
|
||||
@EnvironmentObject var toastStateStore: ToastStateStore
|
||||
@EnvironmentObject var refreshToolbarNotifier: RefreshToolbarNotifier
|
||||
private let state: ListActionsToolbarState
|
||||
private let initialState: ListActionsToolbarState
|
||||
private let itemTypeForActionBar: MailboxItemType
|
||||
private let availableActions: AvailableListToolbarActions
|
||||
private let deleteActions: DeleteActions
|
||||
@@ -54,7 +57,7 @@ private struct ListActionBarViewModifier: ViewModifier {
|
||||
private let readActionPerformerActions: ReadActionPerformerActions
|
||||
|
||||
init(
|
||||
state: ListActionsToolbarState,
|
||||
initialState: ListActionsToolbarState,
|
||||
availableActions: AvailableListToolbarActions,
|
||||
starActionPerformerActions: StarActionPerformerActions = .productionInstance,
|
||||
readActionPerformerActions: ReadActionPerformerActions = .productionInstance,
|
||||
@@ -65,7 +68,7 @@ private struct ListActionBarViewModifier: ViewModifier {
|
||||
selectedItems: Binding<Set<MailboxSelectedItem>>
|
||||
) {
|
||||
self._selectedItems = selectedItems
|
||||
self.state = state
|
||||
self.initialState = initialState
|
||||
self.itemTypeForActionBar = itemTypeForActionBar
|
||||
self.availableActions = availableActions
|
||||
self.deleteActions = deleteActions
|
||||
@@ -78,7 +81,7 @@ private struct ListActionBarViewModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
StoreView(
|
||||
store: ListActionsToolbarStore(
|
||||
state: state,
|
||||
state: initialState,
|
||||
availableActions: availableActions,
|
||||
starActionPerformerActions: starActionPerformerActions,
|
||||
readActionPerformerActions: readActionPerformerActions,
|
||||
@@ -116,16 +119,8 @@ private struct ListActionBarViewModifier: ViewModifier {
|
||||
store.handle(action: .dismissMoveToSheet)
|
||||
}
|
||||
)
|
||||
.sheet(item: store.binding(\.moreActionSheetPresented)) { state in
|
||||
ListActionsToolbarMoreSheet(state: state) { action in
|
||||
store.handle(action: .moreSheetAction(action, ids: selectedItemsIDs))
|
||||
} editToolbarTapped: {
|
||||
store.handle(action: .editToolbarTapped)
|
||||
}
|
||||
.alert(model: store.binding(\.moreDeleteConfirmationAlert))
|
||||
.sheet(isPresented: store.binding(\.isEditToolbarSheetPresented)) {
|
||||
EditToolbarScreen(state: .initial(toolbarType: .list), customizeToolbarService: mailUserSession)
|
||||
}
|
||||
.sheet(isPresented: store.binding(\.isEditToolbarSheetPresented)) {
|
||||
EditToolbarScreen(state: .initial(toolbarType: .list), customizeToolbarService: mailUserSession)
|
||||
}
|
||||
.sheet(isPresented: store.binding(\.isSnoozeSheetPresented)) {
|
||||
SnoozeView(
|
||||
@@ -151,21 +146,14 @@ private struct ListActionBarViewModifier: ViewModifier {
|
||||
selectedItems.map(\.id)
|
||||
}
|
||||
|
||||
private func toolbarContent(
|
||||
state: ListActionsToolbarState,
|
||||
store: ListActionsToolbarStore
|
||||
) -> some ToolbarContent {
|
||||
private func toolbarContent(state: State, store: Store) -> some ToolbarContent {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
HStack {
|
||||
ForEachEnumerated(state.bottomBarActions, id: \.element) { action, index in
|
||||
if index == 0 {
|
||||
Spacer()
|
||||
}
|
||||
Button(action: { store.handle(action: .actionSelected(action, ids: selectedItemsIDs)) }) {
|
||||
action.displayData.image
|
||||
.foregroundStyle(DS.Color.Icon.weak)
|
||||
}
|
||||
.accessibilityIdentifier(MailboxActionBarViewIdentifiers.button(index: index))
|
||||
toolbarItem(for: action, state: state, store: store)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
@@ -176,6 +164,37 @@ private struct ListActionBarViewModifier: ViewModifier {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func toolbarItem(
|
||||
for action: ListActions,
|
||||
state: State,
|
||||
store: Store
|
||||
) -> some View {
|
||||
if action == .more {
|
||||
Menu(
|
||||
content: {
|
||||
ActionMenuButton(displayData: InternalAction.editToolbar.displayData) {
|
||||
store.handle(action: .editToolbarTapped)
|
||||
}
|
||||
Section {
|
||||
ForEach(state.moreSheetOnlyActions.reversed(), id: \.self) { action in
|
||||
ActionMenuButton(displayData: action.displayData) {
|
||||
store.handle(action: .actionSelected(action, ids: selectedItemsIDs))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
label: {
|
||||
action.displayData.image
|
||||
.foregroundStyle(DS.Color.Icon.weak)
|
||||
})
|
||||
} else {
|
||||
Button(action: { store.handle(action: .actionSelected(action, ids: selectedItemsIDs)) }) {
|
||||
action.displayData.image
|
||||
.foregroundStyle(DS.Color.Icon.weak)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Accessibility
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (c) 2024 Proton Technologies AG
|
||||
//
|
||||
// This file is part of Proton Mail.
|
||||
//
|
||||
// Proton Mail 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
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import proton_app_uniffi
|
||||
import InboxCoreUI
|
||||
import InboxDesignSystem
|
||||
import SwiftUI
|
||||
|
||||
struct ListActionsToolbarMoreSheet: View {
|
||||
let state: ListActionsToolbarMoreSheetState
|
||||
let actionTapped: (ListActions) -> Void
|
||||
let editToolbarTapped: () -> Void
|
||||
|
||||
init(
|
||||
state: ListActionsToolbarMoreSheetState,
|
||||
actionTapped: @escaping (ListActions) -> Void,
|
||||
editToolbarTapped: @escaping () -> Void
|
||||
) {
|
||||
self.state = state
|
||||
self.actionTapped = actionTapped
|
||||
self.editToolbarTapped = editToolbarTapped
|
||||
}
|
||||
var body: some View {
|
||||
ClosableScreen {
|
||||
ScrollView {
|
||||
VStack(spacing: DS.Spacing.large) {
|
||||
section(content: state.moreSheetOnlyActions)
|
||||
section(content: state.bottomBarActions)
|
||||
|
||||
editToolbarSection()
|
||||
}
|
||||
.padding(.all, DS.Spacing.large)
|
||||
}
|
||||
.background(DS.Color.Background.secondary)
|
||||
.navigationTitle(L10n.Mailbox.selected(emailsCount: state.selectedItemsIDs.count).string)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func section(content: [ListActions]) -> some View {
|
||||
ActionSheetSection {
|
||||
ForEachLast(collection: content) { action, isLast in
|
||||
ActionSheetImageButton(
|
||||
displayData: action.displayData,
|
||||
displayBottomSeparator: !isLast,
|
||||
action: { actionTapped(action) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func editToolbarSection() -> some View {
|
||||
EditToolbarSheetSection {
|
||||
editToolbarTapped()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ListActionsToolbarMoreSheet(
|
||||
state: ListActionsToolbarMoreSheetPreviewProvider.state(),
|
||||
actionTapped: { _ in },
|
||||
editToolbarTapped: {}
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) 2024 Proton Technologies AG
|
||||
//
|
||||
// This file is part of Proton Mail.
|
||||
//
|
||||
// Proton Mail 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
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ListActionsToolbarMoreSheetPreviewProvider {
|
||||
|
||||
static func state() -> ListActionsToolbarMoreSheetState {
|
||||
.init(
|
||||
selectedItemsIDs: [.init(value: 1), .init(value: 2), .init(value: 3)],
|
||||
bottomBarActions: [
|
||||
.markUnread,
|
||||
.moveToSystemFolder(.init(localId: .init(value: 4), name: .archive)),
|
||||
.moveToSystemFolder(.init(localId: .init(value: 5), name: .inbox)),
|
||||
.moveToSystemFolder(.init(localId: .init(value: 6), name: .trash)),
|
||||
.star,
|
||||
],
|
||||
moreSheetOnlyActions: [
|
||||
.labelAs,
|
||||
.moveTo,
|
||||
.moveToSystemFolder(.init(localId: .init(value: 7), name: .spam)),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import InboxCoreUI
|
||||
import proton_app_uniffi
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class ListActionsToolbarStore: StateStore {
|
||||
@Published var state: ListActionsToolbarState
|
||||
|
||||
@@ -89,50 +88,34 @@ final class ListActionsToolbarStore: StateStore {
|
||||
|
||||
private func handle(action: ListActions, ids: [ID]) async {
|
||||
switch action {
|
||||
case .more:
|
||||
let moreActionSheetState = ListActionsToolbarMoreSheetState(
|
||||
selectedItemsIDs: ids,
|
||||
bottomBarActions: state.bottomBarActions.moreActionFiltered,
|
||||
moreSheetOnlyActions: state.moreSheetOnlyActions
|
||||
)
|
||||
state =
|
||||
state
|
||||
.copy(\.moreActionSheetPresented, to: moreActionSheetState)
|
||||
case .labelAs:
|
||||
dismissMoreActionSheet()
|
||||
state =
|
||||
state
|
||||
.copy(\.labelAsSheetPresented, to: .init(sheetType: .labelAs, ids: ids, mailboxItem: itemTypeForActionBar.mailboxItem))
|
||||
case .moveTo:
|
||||
dismissMoreActionSheet()
|
||||
state =
|
||||
state
|
||||
.copy(\.moveToSheetPresented, to: .init(sheetType: .moveTo, ids: ids, mailboxItem: itemTypeForActionBar.mailboxItem))
|
||||
case .star:
|
||||
dismissMoreActionSheet()
|
||||
await starActionPerformer.star(itemsWithIDs: ids, itemType: itemTypeForActionBar)
|
||||
case .unstar:
|
||||
dismissMoreActionSheet()
|
||||
await starActionPerformer.unstar(itemsWithIDs: ids, itemType: itemTypeForActionBar)
|
||||
case .markRead:
|
||||
dismissMoreActionSheet()
|
||||
await readActionPerformer.markAsRead(itemsWithIDs: ids, itemType: itemTypeForActionBar)
|
||||
case .markUnread:
|
||||
dismissMoreActionSheet()
|
||||
await readActionPerformer.markAsUnread(itemsWithIDs: ids, itemType: itemTypeForActionBar)
|
||||
case .permanentDelete:
|
||||
let keyPath: WritableKeyPath<ListActionsToolbarState, AlertModel?> =
|
||||
state.moreActionSheetPresented != nil ? \.moreDeleteConfirmationAlert : \.deleteConfirmationAlert
|
||||
let alert: AlertModel = .deleteConfirmation(
|
||||
itemsCount: ids.count,
|
||||
action: { [weak self] action in self?.handle(action: .alertActionTapped(action, ids: ids)) }
|
||||
)
|
||||
state = state.copy(keyPath, to: alert)
|
||||
state = state.copy(\.deleteConfirmationAlert, to: alert)
|
||||
case .moveToSystemFolder(let model), .notSpam(let model):
|
||||
await performMoveToAction(destination: model, ids: ids)
|
||||
case .snooze:
|
||||
dismissMoreActionSheet()
|
||||
state = state.copy(\.isSnoozeSheetPresented, to: true)
|
||||
case .more:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +141,6 @@ final class ListActionsToolbarStore: StateStore {
|
||||
state =
|
||||
state
|
||||
.copy(\.deleteConfirmationAlert, to: nil)
|
||||
.copy(\.moreDeleteConfirmationAlert, to: nil)
|
||||
switch action {
|
||||
case .delete:
|
||||
await deleteActionsPerformer.delete(itemsWithIDs: ids, itemType: itemType)
|
||||
@@ -182,13 +164,8 @@ final class ListActionsToolbarStore: StateStore {
|
||||
.copy(\.moreSheetOnlyActions, to: actions.hiddenListActions)
|
||||
}
|
||||
|
||||
private func dismissMoreActionSheet() {
|
||||
state = state.copy(\.moreActionSheetPresented, to: nil)
|
||||
}
|
||||
|
||||
private func itemDeleted() {
|
||||
toastStateStore.present(toast: .deleted())
|
||||
dismissMoreActionSheet()
|
||||
}
|
||||
|
||||
private func handleMoveActionSuccess(
|
||||
@@ -199,12 +176,10 @@ final class ListActionsToolbarStore: StateStore {
|
||||
let destinationName = destination.name.displayData.title.string
|
||||
let toast: Toast = .moveTo(id: toastID, destinationName: destinationName, undoAction: undoAction)
|
||||
toastStateStore.present(toast: toast)
|
||||
dismissMoreActionSheet()
|
||||
}
|
||||
|
||||
private func handleMoveActionFailure(error: Error) {
|
||||
toastStateStore.present(toast: .error(message: error.localizedDescription))
|
||||
dismissMoreActionSheet()
|
||||
}
|
||||
|
||||
private func dismissToast(withID toastID: UUID) {
|
||||
|
||||
@@ -22,11 +22,9 @@ import InboxCoreUI
|
||||
struct ListActionsToolbarState: Copying, Equatable {
|
||||
var bottomBarActions: [ListActions]
|
||||
var moreSheetOnlyActions: [ListActions]
|
||||
var moreActionSheetPresented: ListActionsToolbarMoreSheetState?
|
||||
var labelAsSheetPresented: ActionSheetInput?
|
||||
var moveToSheetPresented: ActionSheetInput?
|
||||
var deleteConfirmationAlert: AlertModel?
|
||||
var moreDeleteConfirmationAlert: AlertModel?
|
||||
var isSnoozeSheetPresented: Bool
|
||||
var isEditToolbarSheetPresented: Bool
|
||||
}
|
||||
@@ -36,11 +34,9 @@ extension ListActionsToolbarState {
|
||||
.init(
|
||||
bottomBarActions: [],
|
||||
moreSheetOnlyActions: [],
|
||||
moreActionSheetPresented: nil,
|
||||
labelAsSheetPresented: nil,
|
||||
moveToSheetPresented: nil,
|
||||
deleteConfirmationAlert: nil,
|
||||
moreDeleteConfirmationAlert: nil,
|
||||
isSnoozeSheetPresented: false,
|
||||
isEditToolbarSheetPresented: false
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import proton_app_uniffi
|
||||
import InboxDesignSystem
|
||||
|
||||
extension MovableSystemFolder {
|
||||
var displayData: ActionDisplayData {
|
||||
|
||||
@@ -22,3 +22,9 @@ struct MoveToState: Copying {
|
||||
var moveToCustomFolderActions: [MoveToCustomFolder]
|
||||
var createFolderLabelPresented: Bool
|
||||
}
|
||||
|
||||
extension MoveToState {
|
||||
static var initial: Self {
|
||||
.init(moveToSystemFolderActions: [], moveToCustomFolderActions: [], createFolderLabelPresented: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import InboxCore
|
||||
|
||||
struct MoveToSheet: View {
|
||||
@EnvironmentObject var toastStateStore: ToastStateStore
|
||||
private let initialState: MoveToState
|
||||
private let input: ActionSheetInput
|
||||
private let mailbox: Mailbox
|
||||
private let availableMoveToActions: AvailableMoveToActions
|
||||
@@ -31,6 +32,7 @@ struct MoveToSheet: View {
|
||||
private let mailUserSession: MailUserSession
|
||||
|
||||
init(
|
||||
initialState: MoveToState = .initial,
|
||||
input: ActionSheetInput,
|
||||
mailbox: Mailbox,
|
||||
availableMoveToActions: AvailableMoveToActions,
|
||||
@@ -38,6 +40,7 @@ struct MoveToSheet: View {
|
||||
navigation: @escaping (MoveToSheetNavigation) -> Void,
|
||||
mailUserSession: MailUserSession
|
||||
) {
|
||||
self.initialState = initialState
|
||||
self.input = input
|
||||
self.mailbox = mailbox
|
||||
self.availableMoveToActions = availableMoveToActions
|
||||
@@ -49,6 +52,7 @@ struct MoveToSheet: View {
|
||||
var body: some View {
|
||||
StoreView(
|
||||
store: MoveToSheetStateStore(
|
||||
state: initialState,
|
||||
input: input,
|
||||
mailbox: mailbox,
|
||||
availableMoveToActions: availableMoveToActions,
|
||||
|
||||
@@ -23,25 +23,42 @@ enum MoveToSheetPreviewProvider {
|
||||
static var availableMoveToActions: AvailableMoveToActions {
|
||||
.init(
|
||||
message: { _, _ in
|
||||
.ok([
|
||||
.systemFolder(.init(localId: .init(value: 1), name: .inbox)),
|
||||
.systemFolder(.init(localId: .init(value: 2), name: .archive)),
|
||||
.customFolder(customFoldersTree),
|
||||
.customFolder(
|
||||
.init(
|
||||
localId: .init(value: 6),
|
||||
name: "4",
|
||||
color: .init(value: "#9E221A"),
|
||||
children: []
|
||||
)),
|
||||
])
|
||||
.ok(systemFolderActions + customFolderActions)
|
||||
},
|
||||
conversation: { _, _ in .ok([]) }
|
||||
)
|
||||
}
|
||||
|
||||
static func state() -> MoveToState {
|
||||
.init(
|
||||
moveToSystemFolderActions: systemFolderActions.compactMap(\.moveToSystemFolder),
|
||||
moveToCustomFolderActions: customFolderActions.compactMap(\.moveToCustomFolder),
|
||||
createFolderLabelPresented: false
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private static var systemFolderActions: [MoveAction] {
|
||||
[
|
||||
.systemFolder(.init(localId: .init(value: 1), name: .inbox)),
|
||||
.systemFolder(.init(localId: .init(value: 2), name: .archive)),
|
||||
]
|
||||
}
|
||||
|
||||
private static var customFolderActions: [MoveAction] {
|
||||
[
|
||||
.customFolder(customFoldersTree),
|
||||
.customFolder(
|
||||
.init(
|
||||
localId: .init(value: 6),
|
||||
name: "4",
|
||||
color: .init(value: "#9E221A"),
|
||||
children: []
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
private static var customFoldersTree: CustomFolderAction {
|
||||
.init(
|
||||
localId: .init(value: 3),
|
||||
|
||||
@@ -23,7 +23,7 @@ import proton_app_uniffi
|
||||
import SwiftUI
|
||||
|
||||
class MoveToSheetStateStore: StateStore {
|
||||
@Published var state: MoveToState = .initial
|
||||
@Published var state: MoveToState
|
||||
|
||||
private let input: ActionSheetInput
|
||||
private let moveToActionsProvider: MoveToActionsProvider
|
||||
@@ -33,6 +33,7 @@ class MoveToSheetStateStore: StateStore {
|
||||
private let mailUserSession: MailUserSession
|
||||
|
||||
init(
|
||||
state: MoveToState,
|
||||
input: ActionSheetInput,
|
||||
mailbox: Mailbox,
|
||||
availableMoveToActions: AvailableMoveToActions,
|
||||
@@ -41,6 +42,7 @@ class MoveToSheetStateStore: StateStore {
|
||||
navigation: @escaping (MoveToSheetNavigation) -> Void,
|
||||
mailUserSession: MailUserSession
|
||||
) {
|
||||
self.state = state
|
||||
self.input = input
|
||||
self.moveToActionsProvider = .init(mailbox: mailbox, availableMoveToActions: availableMoveToActions)
|
||||
self.toastStateStore = toastStateStore
|
||||
@@ -49,14 +51,14 @@ class MoveToSheetStateStore: StateStore {
|
||||
self.mailUserSession = mailUserSession
|
||||
}
|
||||
|
||||
func handle(action: MoveToSheetAction) {
|
||||
func handle(action: MoveToSheetAction) async {
|
||||
switch action {
|
||||
case .viewAppear:
|
||||
loadMoveToActions()
|
||||
await loadMoveToActions()
|
||||
case .customFolderTapped(let customFolder):
|
||||
moveTo(destinationID: customFolder.id, destinationName: customFolder.name)
|
||||
await moveTo(destinationID: customFolder.id, destinationName: customFolder.name)
|
||||
case .systemFolderTapped(let systemFolder):
|
||||
moveTo(destinationID: systemFolder.id, destinationName: systemFolder.label.displayData.title.string)
|
||||
await moveTo(destinationID: systemFolder.id, destinationName: systemFolder.label.displayData.title.string)
|
||||
case .createFolderTapped:
|
||||
state = state.copy(\.createFolderLabelPresented, to: true)
|
||||
}
|
||||
@@ -64,51 +66,36 @@ class MoveToSheetStateStore: StateStore {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func moveTo(destinationID: ID, destinationName: String) {
|
||||
Task { [weak self, mailUserSession] in
|
||||
guard let self else { return }
|
||||
|
||||
do {
|
||||
let undo = try await moveToActionPerformer.moveTo(
|
||||
destinationID: destinationID,
|
||||
itemsIDs: input.ids,
|
||||
itemType: input.mailboxItem.itemType
|
||||
)
|
||||
let toastID = UUID()
|
||||
let undoAction = undo.undoAction(userSession: mailUserSession) {
|
||||
self.dismissToast(withID: toastID)
|
||||
}
|
||||
let toast: Toast = .moveTo(id: toastID, destinationName: destinationName, undoAction: undoAction)
|
||||
dismissSheet(presentingToast: toast)
|
||||
} catch {
|
||||
dismissSheet(presentingToast: .error(message: error.localizedDescription))
|
||||
private func moveTo(destinationID: ID, destinationName: String) async {
|
||||
do {
|
||||
let undo = try await moveToActionPerformer.moveTo(
|
||||
destinationID: destinationID,
|
||||
itemsIDs: input.ids,
|
||||
itemType: input.mailboxItem.itemType
|
||||
)
|
||||
let toastID = UUID()
|
||||
let undoAction = undo.undoAction(userSession: mailUserSession) {
|
||||
self.dismissToast(withID: toastID)
|
||||
}
|
||||
let toast: Toast = .moveTo(id: toastID, destinationName: destinationName, undoAction: undoAction)
|
||||
dismissSheet(presentingToast: toast)
|
||||
} catch {
|
||||
dismissSheet(presentingToast: .error(message: error.localizedDescription))
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissToast(withID toastID: UUID) {
|
||||
Dispatcher.dispatchOnMain(
|
||||
.init(block: { [weak self] in
|
||||
self?.toastStateStore.dismiss(withID: toastID)
|
||||
}))
|
||||
toastStateStore.dismiss(withID: toastID)
|
||||
}
|
||||
|
||||
private func dismissSheet(presentingToast toast: Toast) {
|
||||
Dispatcher.dispatchOnMain(
|
||||
.init { [weak self, input] in
|
||||
self?.toastStateStore.present(toast: toast)
|
||||
self?.navigation(input.mailboxItem.shouldGoBack ? .dismissAndGoBack : .dismiss)
|
||||
})
|
||||
toastStateStore.present(toast: toast)
|
||||
navigation(input.mailboxItem.shouldGoBack ? .dismissAndGoBack : .dismiss)
|
||||
}
|
||||
|
||||
private func loadMoveToActions() {
|
||||
Task {
|
||||
let actions = await moveToActionsProvider.actions(for: input.mailboxItem.itemType, ids: input.ids)
|
||||
Dispatcher.dispatchOnMain(
|
||||
.init(block: { [weak self] in
|
||||
self?.update(moveToActions: actions)
|
||||
}))
|
||||
}
|
||||
private func loadMoveToActions() async {
|
||||
let actions = await moveToActionsProvider.actions(for: input.mailboxItem.itemType, ids: input.ids)
|
||||
update(moveToActions: actions)
|
||||
}
|
||||
|
||||
private func update(moveToActions: [MoveAction]) {
|
||||
@@ -119,7 +106,7 @@ class MoveToSheetStateStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
private extension MoveAction {
|
||||
extension MoveAction {
|
||||
|
||||
var moveToSystemFolder: MoveToSystemFolder? {
|
||||
guard case .systemFolder(let model) = self else {
|
||||
@@ -149,9 +136,3 @@ private extension CustomFolderAction {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension MoveToState {
|
||||
static var initial: Self {
|
||||
.init(moveToSystemFolderActions: [], moveToCustomFolderActions: [], createFolderLabelPresented: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,19 +18,17 @@
|
||||
import proton_app_uniffi
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
class AppAppearanceStore: ObservableObject {
|
||||
@MainActor
|
||||
static let shared = AppAppearanceStore(mailSession: { AppContext.shared.mailSession })
|
||||
|
||||
@Published var colorScheme: ColorScheme?
|
||||
private let mailSession: () -> MailSessionProtocol
|
||||
|
||||
@MainActor
|
||||
init(mailSession: @escaping () -> MailSessionProtocol) {
|
||||
self.mailSession = mailSession
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func updateColorScheme() async {
|
||||
let appearance = try! await mailSession().getAppSettings().get().appearance
|
||||
switch appearance {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2025 Proton Technologies AG
|
||||
//
|
||||
// This file is part of Proton Mail.
|
||||
//
|
||||
// Proton Mail 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
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum AppIcon: CaseIterable, Hashable {
|
||||
case `default`
|
||||
case notes
|
||||
case weather
|
||||
case calculator
|
||||
|
||||
init(rawValue: String?) {
|
||||
switch rawValue {
|
||||
case Self.appIconNotes: self = .notes
|
||||
case Self.appIconWeather: self = .weather
|
||||
case Self.appIconCalculator: self = .calculator
|
||||
default: self = .default
|
||||
}
|
||||
}
|
||||
|
||||
var title: LocalizedStringResource {
|
||||
switch self {
|
||||
case .default: return L10n.Settings.AppIcon.primary
|
||||
case .notes: return L10n.Settings.AppIcon.notes
|
||||
case .weather: return L10n.Settings.AppIcon.weather
|
||||
case .calculator: return L10n.Settings.AppIcon.calculator
|
||||
}
|
||||
}
|
||||
|
||||
var alternateIconName: String? {
|
||||
switch self {
|
||||
case .default: nil
|
||||
case .notes: Self.appIconNotes
|
||||
case .weather: Self.appIconWeather
|
||||
case .calculator: Self.appIconCalculator
|
||||
}
|
||||
}
|
||||
|
||||
var preview: ImageResource {
|
||||
switch self {
|
||||
case .default: return ImageResource.appIconPreview
|
||||
case .weather: return ImageResource.appIconWeatherPreview
|
||||
case .notes: return ImageResource.appIconNotesPreview
|
||||
case .calculator: return ImageResource.appIconCalculatorPreview
|
||||
}
|
||||
}
|
||||
|
||||
private static let appIconNotes: String = "AppIcon-notes"
|
||||
private static let appIconWeather: String = "AppIcon-weather"
|
||||
private static let appIconCalculator: String = "AppIcon-calculator"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2025 Proton Technologies AG
|
||||
//
|
||||
// This file is part of Proton Mail.
|
||||
//
|
||||
// Proton Mail 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
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol AppIconConfigurable {
|
||||
var alternateIconName: String? { get }
|
||||
var supportsAlternateIcons: Bool { get }
|
||||
|
||||
@MainActor
|
||||
func setAlternateIconName(_ alternateIconName: String?) async throws
|
||||
}
|
||||
|
||||
extension UIApplication: AppIconConfigurable {}
|
||||
@@ -44,7 +44,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
self.laContext = laContext
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handle(action: AppProtectionSelectionAction) async {
|
||||
switch action {
|
||||
case .onAppear:
|
||||
@@ -73,7 +72,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func reloadProtectionData() async {
|
||||
guard let settings = await currentAppSettings() else { return }
|
||||
let protection = settings.protection
|
||||
@@ -82,7 +80,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
.copy(\.autoLock, to: settings.autoLock)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func disableProtection() async {
|
||||
switch state.currentProtection {
|
||||
case .biometrics:
|
||||
@@ -94,7 +91,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func disableBiometricProtection() async {
|
||||
guard await biometricAuthDidSucceed() else { return }
|
||||
do {
|
||||
@@ -104,7 +100,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func enableBiometricProtection() async {
|
||||
switch state.currentProtection {
|
||||
case .none:
|
||||
@@ -120,7 +115,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func setPINProtection() async {
|
||||
switch state.currentProtection {
|
||||
case .none:
|
||||
@@ -134,7 +128,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func currentAppSettings() async -> AppSettings? {
|
||||
do {
|
||||
return try await appSettingsRepository.getAppSettings().get()
|
||||
@@ -144,7 +137,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func availableAppProtectionMethods(selected: AppProtection?) -> [AppProtectionMethodViewModel] {
|
||||
let availableMethods: [AppProtectionMethodViewModel.MethodType] =
|
||||
[.none, .pin] + [supportedBiometry()].compactMap { $0 }
|
||||
@@ -157,7 +149,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func supportedBiometry() -> AppProtectionMethodViewModel.MethodType? {
|
||||
switch SupportedBiometry.configuredOnDevice(context: laContext()) {
|
||||
case .none:
|
||||
@@ -169,7 +160,6 @@ class AppProtectionSelectionStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func biometricAuthDidSucceed() async -> Bool {
|
||||
await biometricAuthenticator.authenticate().isSuccess
|
||||
}
|
||||
|
||||
@@ -26,4 +26,5 @@ enum AppSettingsAction {
|
||||
case appearanceSelected(AppAppearance)
|
||||
case combinedContactsChanged(Bool)
|
||||
case alternativeRoutingChanged(Bool)
|
||||
case appIconSelected(AppIcon)
|
||||
}
|
||||
|
||||
@@ -27,17 +27,21 @@ struct AppSettingsScreen: View {
|
||||
@EnvironmentObject var appAppearanceStore: AppAppearanceStore
|
||||
@EnvironmentObject var router: Router<SettingsRoute>
|
||||
@StateObject var store: AppSettingsStateStore
|
||||
private let appIconConfigurator: AppIconConfigurable
|
||||
|
||||
init(
|
||||
state: AppSettingsState = .initial,
|
||||
state: AppSettingsState? = .none,
|
||||
appIconConfigurator: AppIconConfigurable = UIApplication.shared,
|
||||
appSettingsRepository: AppSettingsRepository = AppContext.shared.mailSession
|
||||
) {
|
||||
_store = .init(
|
||||
wrappedValue: .init(
|
||||
state: state,
|
||||
appSettingsRepository: appSettingsRepository
|
||||
state: state ?? .initial(appIconName: appIconConfigurator.alternateIconName),
|
||||
appSettingsRepository: appSettingsRepository,
|
||||
appIconConfigurator: appIconConfigurator
|
||||
)
|
||||
)
|
||||
self.appIconConfigurator = appIconConfigurator
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -69,6 +73,9 @@ struct AppSettingsScreen: View {
|
||||
value: store.state.storedAppSettings.protection.humanReadable.string,
|
||||
action: { router.go(to: .appProtection) }
|
||||
)
|
||||
if appIconConfigurator.supportsAlternateIcons {
|
||||
appIconButton
|
||||
}
|
||||
}
|
||||
FormSection(footer: L10n.Settings.App.combinedContactsInfo) {
|
||||
FormSwitchView(
|
||||
@@ -159,6 +166,30 @@ struct AppSettingsScreen: View {
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var appIconButton: some View {
|
||||
Menu(
|
||||
content: {
|
||||
ForEach(AppIcon.allCases.filter { icon in store.state.appIcon != icon }, id: \.self) { icon in
|
||||
Button(action: { store.handle(action: .appIconSelected(icon)) }) {
|
||||
HStack(spacing: DS.Spacing.medium) {
|
||||
Text(icon.title)
|
||||
Image(icon.preview)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
label: {
|
||||
FormBigButton(
|
||||
title: L10n.Settings.AppIcon.buttonTitle,
|
||||
symbol: .chevronUpChevronDown,
|
||||
value: store.state.appIcon.title.string,
|
||||
action: {}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var combinedContactsBinding: Binding<Bool> {
|
||||
.init(
|
||||
get: { store.state.storedAppSettings.useCombineContacts },
|
||||
@@ -187,7 +218,7 @@ struct AppSettingsScreen: View {
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
AppSettingsScreen(state: .initial)
|
||||
AppSettingsScreen(state: .initial(appIconName: .none))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,16 +19,17 @@ import proton_app_uniffi
|
||||
import InboxCore
|
||||
import SwiftUI
|
||||
|
||||
struct AppSettingsState: Copying {
|
||||
struct AppSettingsState: Copying, Equatable {
|
||||
var areNotificationsEnabled: Bool
|
||||
var appLanguage: String
|
||||
var storedAppSettings: AppSettings
|
||||
var appIcon: AppIcon
|
||||
var isAppearanceMenuShown: Bool
|
||||
}
|
||||
|
||||
extension AppSettingsState {
|
||||
|
||||
static var initial: Self {
|
||||
static func initial(appIconName: String?) -> Self {
|
||||
.init(
|
||||
areNotificationsEnabled: false,
|
||||
appLanguage: .empty,
|
||||
@@ -39,6 +40,7 @@ extension AppSettingsState {
|
||||
useCombineContacts: false,
|
||||
useAlternativeRouting: true
|
||||
),
|
||||
appIcon: AppIcon(rawValue: appIconName),
|
||||
isAppearanceMenuShown: false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,13 +26,14 @@ final class AppSettingsStateStore: StateStore, Sendable {
|
||||
private let notificationCenter: UserNotificationCenter
|
||||
private let urlOpener: URLOpener
|
||||
private let appLangaugeProvider: AppLangaugeProvider
|
||||
private let appIconConfigurator: AppIconConfigurable
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
state: AppSettingsState,
|
||||
appSettingsRepository: AppSettingsRepository,
|
||||
notificationCenter: UserNotificationCenter = UNUserNotificationCenter.current(),
|
||||
urlOpener: URLOpener = UIApplication.shared,
|
||||
appIconConfigurator: AppIconConfigurable,
|
||||
currentLocale: Locale = Locale.current,
|
||||
mainBundle: Bundle = Bundle.main
|
||||
) {
|
||||
@@ -40,10 +41,10 @@ final class AppSettingsStateStore: StateStore, Sendable {
|
||||
self.appSettingsRepository = appSettingsRepository
|
||||
self.notificationCenter = notificationCenter
|
||||
self.urlOpener = urlOpener
|
||||
self.appIconConfigurator = appIconConfigurator
|
||||
self.appLangaugeProvider = .init(currentLocale: currentLocale, mainBundle: mainBundle)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handle(action: AppSettingsAction) async {
|
||||
switch action {
|
||||
case .notificationButtonTapped:
|
||||
@@ -63,9 +64,21 @@ final class AppSettingsStateStore: StateStore, Sendable {
|
||||
await update(setting: \.useCombineContacts, value: value)
|
||||
case .alternativeRoutingChanged(let value):
|
||||
await update(setting: \.useAlternativeRouting, value: value)
|
||||
case .appIconSelected(let appIcon):
|
||||
await updateAppIcon(appIcon)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func updateAppIcon(_ icon: AppIcon) async {
|
||||
guard appIconConfigurator.supportsAlternateIcons else {
|
||||
return
|
||||
}
|
||||
|
||||
try? await appIconConfigurator.setAlternateIconName(icon.alternateIconName)
|
||||
state = state.copy(\.appIcon, to: AppIcon(rawValue: icon.alternateIconName))
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func update<Value>(setting: WritableKeyPath<AppSettingsDiff, Value>, value: Value) async {
|
||||
@@ -84,7 +97,6 @@ final class AppSettingsStateStore: StateStore, Sendable {
|
||||
await refreshStoredAppSettings()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func refreshDeviceSettings() async {
|
||||
let areNotificationsEnabled = await areNotificationsEnabled()
|
||||
state =
|
||||
@@ -93,7 +105,6 @@ final class AppSettingsStateStore: StateStore, Sendable {
|
||||
.copy(\.appLanguage, to: appLangaugeProvider.appLangauge)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func refreshStoredAppSettings() async {
|
||||
do {
|
||||
let settings = try await appSettingsRepository.getAppSettings().get()
|
||||
@@ -131,7 +142,6 @@ final class AppSettingsStateStore: StateStore, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func openNativeAppSettings() async {
|
||||
await urlOpener.open(.settings, options: [:])
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ class AutoLockStore: StateStore {
|
||||
self.router = router
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handle(action: AutoLockAction) async {
|
||||
switch action {
|
||||
case .optionSelected(let autoLock):
|
||||
|
||||
@@ -129,7 +129,9 @@ private extension ToolbarWithActions {
|
||||
switch self {
|
||||
case .list:
|
||||
L10n.Settings.CustomizeToolbars.listToolbarSectionTitle
|
||||
case .message, .conversation:
|
||||
case .message:
|
||||
L10n.Settings.CustomizeToolbars.messageToolbarSectionTitle
|
||||
case .conversation:
|
||||
L10n.Settings.CustomizeToolbars.conversationToolbarSectionTitle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import Combine
|
||||
import InboxCoreUI
|
||||
import InboxCore
|
||||
|
||||
@MainActor
|
||||
class CustomizeToolbarsStore: StateStore {
|
||||
@Published var state: CustomizeToolbarState
|
||||
private let customizeToolbarRepository: CustomizeToolbarRepository
|
||||
|
||||
@@ -19,7 +19,6 @@ import Combine
|
||||
import InboxCore
|
||||
import InboxCoreUI
|
||||
|
||||
@MainActor
|
||||
class EditToolbarStore: StateStore {
|
||||
@Published var state: EditToolbarState
|
||||
private let customizeToolbarRepository: CustomizeToolbarRepository
|
||||
|
||||
@@ -19,7 +19,6 @@ import SwiftUI
|
||||
import InboxCore
|
||||
import InboxCoreUI
|
||||
|
||||
@MainActor
|
||||
class PINStateStore: StateStore {
|
||||
@Published var state: PINScreenState
|
||||
private let pinScreenValidator: PINValidator
|
||||
|
||||
@@ -67,7 +67,6 @@ final class MessageBodyStateStore: StateStore {
|
||||
self.backOnlineActionExecutor = backOnlineActionExecutor
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handle(action: Action) async {
|
||||
switch action {
|
||||
case .onLoad:
|
||||
@@ -117,7 +116,6 @@ final class MessageBodyStateStore: StateStore {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@MainActor
|
||||
private func loadMessageBody(with options: TransformOpts) async {
|
||||
switch await provider.messageBody(forMessageID: messageID, with: options) {
|
||||
case .success(let body):
|
||||
@@ -130,7 +128,6 @@ final class MessageBodyStateStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func reloadContentWhenBackOnline(options: TransformOpts) {
|
||||
backOnlineActionExecutor.execute { [weak self] in
|
||||
guard let self else { return }
|
||||
@@ -139,7 +136,6 @@ final class MessageBodyStateStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func markAsLegitimate(with options: TransformOpts) async {
|
||||
await executeAndReloadMessage(
|
||||
operation: { await legitMessageMarker.markAsLegitimate(forMessageID: messageID) },
|
||||
@@ -147,7 +143,6 @@ final class MessageBodyStateStore: StateStore {
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func unblockSender(emailAddress: String, with options: TransformOpts) async {
|
||||
await executeAndReloadMessage(
|
||||
operation: { await senderUnblocker.unblock(emailAddress: emailAddress) },
|
||||
@@ -155,7 +150,6 @@ final class MessageBodyStateStore: StateStore {
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func unsubscribeNewsletter(with newsletterService: UnsubscribeNewsletter, options: TransformOpts) async {
|
||||
await executeAndReloadMessage(
|
||||
operation: { await newsletterService.unsubscribeFromNewsletter() },
|
||||
@@ -164,7 +158,6 @@ final class MessageBodyStateStore: StateStore {
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func executeAndReloadMessage(
|
||||
operation: () async -> VoidActionResult,
|
||||
with options: TransformOpts,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import OrderedCollections
|
||||
import InboxCore
|
||||
import InboxCoreUI
|
||||
import InboxDesignSystem
|
||||
import InboxRSVP
|
||||
@@ -31,6 +32,7 @@ struct MessageBodyView: View {
|
||||
let mailbox: Mailbox
|
||||
let editScheduledMessage: () -> Void
|
||||
let unsnoozeConversation: () -> Void
|
||||
let draftPresenter: RecipientDraftPresenter
|
||||
@Binding var isBodyLoaded: Bool
|
||||
@Binding var attachmentIDToOpen: ID?
|
||||
@State var bodyContentHeight: CGFloat = .zero
|
||||
@@ -43,7 +45,8 @@ struct MessageBodyView: View {
|
||||
isBodyLoaded: Binding<Bool>,
|
||||
attachmentIDToOpen: Binding<ID?>,
|
||||
editScheduledMessage: @escaping () -> Void,
|
||||
unsnoozeConversation: @escaping () -> Void
|
||||
unsnoozeConversation: @escaping () -> Void,
|
||||
draftPresenter: RecipientDraftPresenter
|
||||
) {
|
||||
self.messageID = messageID
|
||||
self.emailAddress = emailAddress
|
||||
@@ -53,6 +56,7 @@ struct MessageBodyView: View {
|
||||
self._attachmentIDToOpen = attachmentIDToOpen
|
||||
self.editScheduledMessage = editScheduledMessage
|
||||
self.unsnoozeConversation = unsnoozeConversation
|
||||
self.draftPresenter = draftPresenter
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -67,7 +71,7 @@ struct MessageBodyView: View {
|
||||
) { state, store in
|
||||
VStack(spacing: .zero) {
|
||||
if case .loaded(let body) = state.body, let rsvpServiceProvider = body.rsvpServiceProvider {
|
||||
RSVPView(serviceProvider: rsvpServiceProvider)
|
||||
RSVPView(serviceProvider: rsvpServiceProvider, draftPresenter: draftPresenter)
|
||||
}
|
||||
if case .loaded(let body) = state.body, !body.banners.isEmpty {
|
||||
MessageBannersView(
|
||||
|
||||
@@ -16,14 +16,18 @@
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Collections
|
||||
import InboxCore
|
||||
import InboxCoreUI
|
||||
import InboxDesignSystem
|
||||
import proton_app_uniffi
|
||||
import SwiftUI
|
||||
import InboxCoreUI
|
||||
|
||||
struct ExpandedMessageCell: View {
|
||||
private let mailbox: Mailbox
|
||||
private let mailUserSession: MailUserSession
|
||||
private let uiModel: ExpandedMessageCellUIModel
|
||||
private let draftPresenter: RecipientDraftPresenter
|
||||
private let messageAppearanceOverrideStore: MessageAppearanceOverrideStore
|
||||
private let onEvent: (ExpandedMessageCellEvent) -> Void
|
||||
private let htmlLoaded: () -> Void
|
||||
private let areActionsHidden: Bool
|
||||
@@ -37,14 +41,20 @@ struct ExpandedMessageCell: View {
|
||||
|
||||
init(
|
||||
mailbox: Mailbox,
|
||||
mailUserSession: MailUserSession,
|
||||
uiModel: ExpandedMessageCellUIModel,
|
||||
draftPresenter: RecipientDraftPresenter,
|
||||
messageAppearanceOverrideStore: MessageAppearanceOverrideStore,
|
||||
areActionsHidden: Bool,
|
||||
attachmentIDToOpen: Binding<ID?>,
|
||||
onEvent: @escaping (ExpandedMessageCellEvent) -> Void,
|
||||
htmlLoaded: @escaping () -> Void
|
||||
htmlLoaded: @escaping () -> Void,
|
||||
) {
|
||||
self.mailbox = mailbox
|
||||
self.mailUserSession = mailUserSession
|
||||
self.uiModel = uiModel
|
||||
self.draftPresenter = draftPresenter
|
||||
self.messageAppearanceOverrideStore = messageAppearanceOverrideStore
|
||||
self.areActionsHidden = areActionsHidden
|
||||
self._attachmentIDToOpen = attachmentIDToOpen
|
||||
self.onEvent = onEvent
|
||||
@@ -55,6 +65,9 @@ struct ExpandedMessageCell: View {
|
||||
VStack(spacing: .zero) {
|
||||
MessageDetailsView(
|
||||
uiModel: uiModel.messageDetails,
|
||||
mailbox: mailbox,
|
||||
mailUserSession: mailUserSession,
|
||||
messageAppearanceOverrideStore: messageAppearanceOverrideStore,
|
||||
actionButtonsState: actionButtonsState,
|
||||
onEvent: { event in
|
||||
switch event {
|
||||
@@ -64,12 +77,14 @@ struct ExpandedMessageCell: View {
|
||||
onEvent(.onReply)
|
||||
case .onReplyAll:
|
||||
onEvent(.onReplyAll)
|
||||
case .onMoreActions:
|
||||
onEvent(.onMoreActions)
|
||||
case .onMessageAction(let action):
|
||||
onEvent(.onMessageAction(action))
|
||||
case .onSenderTap:
|
||||
onEvent(.onSenderTap)
|
||||
case .onRecipientTap(let recipient):
|
||||
onEvent(.onRecipientTap(recipient))
|
||||
case .onEditToolbar:
|
||||
onEvent(.onEditToolbar)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -81,7 +96,8 @@ struct ExpandedMessageCell: View {
|
||||
isBodyLoaded: $isBodyLoaded,
|
||||
attachmentIDToOpen: $attachmentIDToOpen,
|
||||
editScheduledMessage: { onEvent(.onEditScheduledMessage) },
|
||||
unsnoozeConversation: { onEvent(.unsnoozeConversation) }
|
||||
unsnoozeConversation: { onEvent(.unsnoozeConversation) },
|
||||
draftPresenter: draftPresenter
|
||||
)
|
||||
if !areActionsHidden {
|
||||
MessageActionButtonsView(
|
||||
@@ -123,13 +139,15 @@ enum ExpandedMessageCellEvent {
|
||||
case onReply
|
||||
case onReplyAll
|
||||
case onForward
|
||||
case onMoreActions
|
||||
|
||||
case onSenderTap
|
||||
case onRecipientTap(MessageDetail.Recipient)
|
||||
|
||||
case onEditScheduledMessage
|
||||
case unsnoozeConversation
|
||||
|
||||
case onEditToolbar
|
||||
case onMessageAction(MessageAction)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@@ -143,11 +161,14 @@ enum ExpandedMessageCellEvent {
|
||||
return VStack(spacing: 0) {
|
||||
ExpandedMessageCell(
|
||||
mailbox: .dummy,
|
||||
mailUserSession: .dummy,
|
||||
uiModel: .init(
|
||||
id: .init(value: 0),
|
||||
unread: false,
|
||||
messageDetails: messageDetails
|
||||
),
|
||||
draftPresenter: DraftPresenter.dummy(),
|
||||
messageAppearanceOverrideStore: .init(),
|
||||
areActionsHidden: false,
|
||||
attachmentIDToOpen: .constant(nil),
|
||||
onEvent: { _ in },
|
||||
@@ -155,11 +176,14 @@ enum ExpandedMessageCellEvent {
|
||||
)
|
||||
ExpandedMessageCell(
|
||||
mailbox: .dummy,
|
||||
mailUserSession: .dummy,
|
||||
uiModel: .init(
|
||||
id: .init(value: 1),
|
||||
unread: false,
|
||||
messageDetails: messageDetails
|
||||
),
|
||||
draftPresenter: DraftPresenter.dummy(),
|
||||
messageAppearanceOverrideStore: .init(),
|
||||
areActionsHidden: false,
|
||||
attachmentIDToOpen: .constant(nil),
|
||||
onEvent: { _ in },
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
|
||||
import InboxCoreUI
|
||||
import InboxDesignSystem
|
||||
import enum proton_app_uniffi.ExclusiveLocation
|
||||
import enum proton_app_uniffi.MessageBanner
|
||||
import proton_app_uniffi
|
||||
import SwiftUI
|
||||
|
||||
struct MessageDetailsView: View {
|
||||
@@ -33,6 +32,9 @@ struct MessageDetailsView: View {
|
||||
|
||||
@State private(set) var isHeaderCollapsed: Bool = true
|
||||
let uiModel: MessageDetailsUIModel
|
||||
let mailbox: Mailbox
|
||||
let mailUserSession: MailUserSession
|
||||
let messageAppearanceOverrideStore: MessageAppearanceOverrideStore
|
||||
let actionButtonsState: ActionButtonsState
|
||||
let onEvent: (MessageDetailsEvent) -> Void
|
||||
|
||||
@@ -226,11 +228,25 @@ struct MessageDetailsView: View {
|
||||
action: { onEvent(uiModel.isSingleRecipient ? .onReply : .onReplyAll) },
|
||||
image: Image(symbol: uiModel.isSingleRecipient ? .reply : .replyAll)
|
||||
)
|
||||
headerActionButton(
|
||||
action: { onEvent(.onMoreActions) },
|
||||
image: DS.Icon.icThreeDotsHorizontal.image
|
||||
)
|
||||
.accessibilityIdentifier(MessageDetailsViewIdentifiers.threeDotsButton)
|
||||
MessageActionsMenu(
|
||||
state: .initial(messageID: uiModel.id, showEditToolbar: false),
|
||||
mailbox: mailbox,
|
||||
mailUserSession: mailUserSession,
|
||||
messageAppearanceOverrideStore: messageAppearanceOverrideStore,
|
||||
actionTapped: { action in onEvent(.onMessageAction(action)) },
|
||||
editToolbarTapped: { onEvent(.onEditToolbar) }
|
||||
) {
|
||||
DS.Icon.icThreeDotsHorizontal.image
|
||||
.square(size: 20)
|
||||
.foregroundStyle(actionButtonsState.isDisabled ? DS.Color.Text.disabled : DS.Color.Text.weak)
|
||||
.square(size: 36)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: DS.Radius.mediumLarge)
|
||||
.stroke(DS.Color.Border.norm, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.disabled(actionButtonsState.isDisabled)
|
||||
.id(uiModel)
|
||||
}
|
||||
.foregroundColor(DS.Color.Icon.weak)
|
||||
}
|
||||
@@ -334,17 +350,9 @@ struct MessageDetailsView: View {
|
||||
}
|
||||
|
||||
private var labelRow: some View {
|
||||
let capsules = uiModel.labels.map { label in
|
||||
CapsuleView(
|
||||
text: label.text.stringResource,
|
||||
color: label.color,
|
||||
style: .label
|
||||
)
|
||||
}
|
||||
|
||||
return HStack(alignment: .center, spacing: DS.Spacing.small) {
|
||||
CapsuleCloudView(
|
||||
subviews: capsules,
|
||||
HStack(alignment: .center, spacing: DS.Spacing.small) {
|
||||
BadgesView(
|
||||
badges: uiModel.labels.map { label in Badge(text: label.text, color: label.color) }
|
||||
)
|
||||
Spacer()
|
||||
}
|
||||
@@ -379,7 +387,8 @@ private enum RecipientGroup {
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageDetailsUIModel: Equatable {
|
||||
struct MessageDetailsUIModel: Hashable {
|
||||
let id: ID
|
||||
let avatar: AvatarUIModel
|
||||
let sender: MessageDetail.Sender
|
||||
let isSenderProtonOfficial: Bool
|
||||
@@ -408,7 +417,7 @@ extension MessageDetailsUIModel {
|
||||
|
||||
enum MessageDetail {
|
||||
|
||||
struct Sender: Equatable {
|
||||
struct Sender: Hashable {
|
||||
let name: String
|
||||
let address: String
|
||||
let encryptionInfo: String
|
||||
@@ -421,11 +430,15 @@ enum MessageDetail {
|
||||
let avatarInfo: AvatarInfo
|
||||
}
|
||||
|
||||
struct Location: Equatable {
|
||||
struct Location: Hashable {
|
||||
let id: ID
|
||||
let name: LocalizedStringResource
|
||||
let icon: Image
|
||||
let iconColor: Color?
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,7 +446,8 @@ enum MessageDetailsEvent {
|
||||
case onTap
|
||||
case onReply
|
||||
case onReplyAll
|
||||
case onMoreActions
|
||||
case onMessageAction(MessageAction)
|
||||
case onEditToolbar
|
||||
case onSenderTap
|
||||
case onRecipientTap(MessageDetail.Recipient)
|
||||
}
|
||||
@@ -455,9 +469,12 @@ extension Array where Element == MessageDetail.Recipient {
|
||||
.init(labelId: .init(value: 3), text: "Summer trip", color: .pink),
|
||||
])
|
||||
|
||||
return MessageDetailsView(
|
||||
MessageDetailsView(
|
||||
isHeaderCollapsed: false,
|
||||
uiModel: model,
|
||||
mailbox: .dummy,
|
||||
mailUserSession: .dummy,
|
||||
messageAppearanceOverrideStore: .init(),
|
||||
actionButtonsState: .enabled,
|
||||
onEvent: { _ in }
|
||||
)
|
||||
@@ -505,6 +522,7 @@ enum MessageDetailsPreviewProvider {
|
||||
recipientsBcc: [MessageDetail.Recipient] = recipientsBcc
|
||||
) -> MessageDetailsUIModel {
|
||||
.init(
|
||||
id: ID.random(),
|
||||
avatar: .init(
|
||||
info: .init(initials: "", color: DS.Color.Background.secondary),
|
||||
type: .sender(params: .init())
|
||||
|
||||
@@ -25,30 +25,51 @@ extension View {
|
||||
|
||||
func conversationBottomToolbar(
|
||||
actions: ConversationToolbarActions?,
|
||||
mailbox: @escaping () -> Mailbox,
|
||||
mailUserSession: MailUserSession,
|
||||
messageAppearanceOverrideStore: MessageAppearanceOverrideStore,
|
||||
editToolbarTapped: @escaping (ToolbarType) -> Void,
|
||||
messageActionSelected: @escaping (MessageAction) -> Void,
|
||||
conversationActionSelected: @escaping (ConversationAction) -> Void
|
||||
) -> some View {
|
||||
modifier(
|
||||
ConversationToolbarModifier(
|
||||
actions: actions,
|
||||
mailbox: mailbox,
|
||||
mailUserSession: mailUserSession,
|
||||
messageAppearanceOverrideStore: messageAppearanceOverrideStore,
|
||||
editToolbarTapped: editToolbarTapped,
|
||||
messageActionSelected: messageActionSelected,
|
||||
conversationActionSelected: conversationActionSelected
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ConversationToolbarModifier: ViewModifier {
|
||||
private let actions: ConversationToolbarActions?
|
||||
private let mailbox: () -> Mailbox
|
||||
private let mailUserSession: MailUserSession
|
||||
private let messageAppearanceOverrideStore: MessageAppearanceOverrideStore
|
||||
private let editToolbarTapped: (ToolbarType) -> Void
|
||||
private let messageActionSelected: (MessageAction) -> Void
|
||||
private let conversationActionSelected: (ConversationAction) -> Void
|
||||
|
||||
init(
|
||||
actions: ConversationToolbarActions?,
|
||||
mailbox: @escaping () -> Mailbox,
|
||||
mailUserSession: MailUserSession,
|
||||
messageAppearanceOverrideStore: MessageAppearanceOverrideStore,
|
||||
editToolbarTapped: @escaping (ToolbarType) -> Void,
|
||||
messageActionSelected: @escaping (MessageAction) -> Void,
|
||||
conversationActionSelected: @escaping (ConversationAction) -> Void
|
||||
) {
|
||||
self.actions = actions
|
||||
self.mailbox = mailbox
|
||||
self.mailUserSession = mailUserSession
|
||||
self.messageAppearanceOverrideStore = messageAppearanceOverrideStore
|
||||
self.editToolbarTapped = editToolbarTapped
|
||||
self.messageActionSelected = messageActionSelected
|
||||
self.conversationActionSelected = conversationActionSelected
|
||||
}
|
||||
@@ -59,29 +80,72 @@ struct ConversationToolbarModifier: ViewModifier {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
if let actions {
|
||||
switch actions {
|
||||
case .message(let actions):
|
||||
toolbarContent(actions: actions.visibleMessageActions, selected: messageActionSelected)
|
||||
case .conversation(let actions):
|
||||
toolbarContent(actions: actions.visibleListActions, selected: conversationActionSelected)
|
||||
case .message(let actions, let messageID):
|
||||
messageToolbarContent(
|
||||
messageID: messageID,
|
||||
actions: actions.visibleMessageActions,
|
||||
)
|
||||
.id(actions)
|
||||
case .conversation(let actions, let conversationID):
|
||||
conversationToolbarContent(
|
||||
conversationID: conversationID,
|
||||
actions: actions.visibleListActions
|
||||
)
|
||||
.id(actions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func toolbarContent<Action: DisplayableAction>(
|
||||
private func messageToolbarContent(messageID: ID, actions: [MessageAction]) -> some View {
|
||||
toolbarContent(actions: actions, selected: messageActionSelected) {
|
||||
MessageActionsMenu(
|
||||
state: .initial(messageID: messageID, showEditToolbar: true),
|
||||
mailbox: mailbox(),
|
||||
mailUserSession: mailUserSession,
|
||||
messageAppearanceOverrideStore: messageAppearanceOverrideStore,
|
||||
actionTapped: messageActionSelected,
|
||||
editToolbarTapped: { editToolbarTapped(.message) }
|
||||
) {
|
||||
InternalAction.more.displayData.image
|
||||
.foregroundStyle(DS.Color.Icon.weak)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func conversationToolbarContent(conversationID: ID, actions: [ConversationAction]) -> some View {
|
||||
toolbarContent(actions: actions, selected: conversationActionSelected) {
|
||||
ConversationActionsMenu(
|
||||
conversationID: conversationID,
|
||||
mailbox: mailbox(),
|
||||
mailUserSession: mailUserSession,
|
||||
actionTapped: conversationActionSelected,
|
||||
editToolbarTapped: { editToolbarTapped(.conversation) }
|
||||
) {
|
||||
InternalAction.more.displayData.image
|
||||
.foregroundStyle(DS.Color.Icon.weak)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func toolbarContent<MoreActionsMenu: View, Action: DisplayableAction>(
|
||||
actions: [Action],
|
||||
selected: @escaping (Action) -> Void
|
||||
selected: @escaping (Action) -> Void,
|
||||
moreActionsMenu: @escaping () -> MoreActionsMenu
|
||||
) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
ForEachEnumerated(actions, id: \.offset) { action, index in
|
||||
if index == 0 {
|
||||
Spacer()
|
||||
}
|
||||
Button(action: { selected(action) }) {
|
||||
action.displayData.image
|
||||
.foregroundStyle(DS.Color.Icon.weak)
|
||||
if action.isMoreAction {
|
||||
moreActionsMenu()
|
||||
} else {
|
||||
Button(action: { selected(action) }) {
|
||||
action.displayData.image
|
||||
.foregroundStyle(DS.Color.Icon.weak)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ struct ConversationDetailListView: View {
|
||||
@ObservedObject private var model: ConversationDetailModel
|
||||
private let mailUserSession: MailUserSession
|
||||
private let draftPresenter: RecipientDraftPresenter
|
||||
private let editToolbar: () -> Void
|
||||
private let goBack: () -> Void
|
||||
|
||||
/// These attributes trigger the different action sheets
|
||||
@@ -36,11 +37,13 @@ struct ConversationDetailListView: View {
|
||||
model: ConversationDetailModel,
|
||||
mailUserSession: MailUserSession,
|
||||
draftPresenter: RecipientDraftPresenter,
|
||||
editToolbar: @escaping () -> Void,
|
||||
goBack: @escaping () -> Void
|
||||
) {
|
||||
self.model = model
|
||||
self.mailUserSession = mailUserSession
|
||||
self.draftPresenter = draftPresenter
|
||||
self.editToolbar = editToolbar
|
||||
self.goBack = goBack
|
||||
}
|
||||
|
||||
@@ -146,7 +149,10 @@ struct ConversationDetailListView: View {
|
||||
private func expandedMessageCell(uiModel: ExpandedMessageCellUIModel) -> some View {
|
||||
ExpandedMessageCell(
|
||||
mailbox: model.mailbox.unsafelyUnwrapped,
|
||||
mailUserSession: mailUserSession,
|
||||
uiModel: uiModel,
|
||||
draftPresenter: draftPresenter,
|
||||
messageAppearanceOverrideStore: model.messageAppearanceOverrideStore,
|
||||
areActionsHidden: model.areActionsHidden,
|
||||
attachmentIDToOpen: $model.attachmentIDToOpen,
|
||||
onEvent: { onExpandedMessageCellEvent($0, uiModel: uiModel) },
|
||||
@@ -165,9 +171,17 @@ struct ConversationDetailListView: View {
|
||||
model.onReplyAllMessage(withId: uiModel.id, toastStateStore: toastStateStore)
|
||||
case .onForward:
|
||||
model.onForwardMessage(withId: uiModel.id, toastStateStore: toastStateStore)
|
||||
case .onMoreActions:
|
||||
let input = MessageActionsSheetInput(id: uiModel.id, title: model.seed.subject, origin: .messageHeader)
|
||||
model.actionSheets = model.actionSheets.copy(\.message, to: input)
|
||||
case .onEditToolbar:
|
||||
editToolbar()
|
||||
case .onMessageAction(let action):
|
||||
Task {
|
||||
await model.handle(
|
||||
action: action,
|
||||
messageID: uiModel.id,
|
||||
toastStateStore: toastStateStore,
|
||||
goBack: goBack
|
||||
)
|
||||
}
|
||||
case .onSenderTap:
|
||||
senderActionTarget = uiModel
|
||||
case .onRecipientTap(let recipient):
|
||||
|
||||
@@ -56,7 +56,11 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
}
|
||||
|
||||
var isBottomBarHidden: Bool {
|
||||
seed.isOutbox || conversationToolbarActions?.isEmpty == true
|
||||
if let conversationToolbarActions {
|
||||
seed.isOutbox || conversationToolbarActions.isEmpty
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
var areActionsHidden: Bool {
|
||||
@@ -252,7 +256,6 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
action: MessageAction,
|
||||
messageID: ID,
|
||||
toastStateStore: ToastStateStore,
|
||||
actionOrigin: ConversationActionOrigin,
|
||||
goBack: @MainActor @escaping () -> Void
|
||||
) async {
|
||||
switch action {
|
||||
@@ -317,7 +320,7 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
)
|
||||
}
|
||||
)
|
||||
present(alert: alert, origin: actionOrigin)
|
||||
actionAlert = alert
|
||||
case .reply:
|
||||
actionSheets = .allSheetsDismissed
|
||||
onReplyMessage(withId: messageID, toastStateStore: toastStateStore)
|
||||
@@ -354,9 +357,9 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
goBack: goBack
|
||||
)
|
||||
})
|
||||
present(alert: alert, origin: actionOrigin)
|
||||
actionAlert = alert
|
||||
case .more:
|
||||
actionSheets = .allSheetsDismissed.copy(\.message, to: .init(id: messageID, title: seed.subject, origin: .toolbar))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +367,6 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
func handle(
|
||||
action: ConversationAction,
|
||||
toastStateStore: ToastStateStore,
|
||||
actionOrigin: ConversationActionOrigin,
|
||||
goBack: @MainActor @escaping () -> Void
|
||||
) async {
|
||||
guard let conversationItem, conversationItem.itemType == .conversation else { return }
|
||||
@@ -380,8 +382,6 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
case .labelAs:
|
||||
actionSheets = .allSheetsDismissed
|
||||
.copy(\.labelAs, to: .init(sheetType: .labelAs, ids: [conversationID], mailboxItem: .conversation))
|
||||
case .more:
|
||||
actionSheets = actionSheets.copy(\.conversation, to: .init(id: conversationID, title: seed.subject))
|
||||
case .moveTo:
|
||||
actionSheets = .allSheetsDismissed
|
||||
.copy(\.moveTo, to: .init(sheetType: .moveTo, ids: [conversationID], mailboxItem: .conversation))
|
||||
@@ -406,7 +406,7 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
)
|
||||
}
|
||||
)
|
||||
present(alert: alert, origin: actionOrigin)
|
||||
actionAlert = alert
|
||||
case .star:
|
||||
await starActionPerformer.star(itemsWithIDs: [conversationID], itemType: .conversation)
|
||||
actionSheets = .allSheetsDismissed
|
||||
@@ -415,6 +415,8 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
actionSheets = .allSheetsDismissed
|
||||
case .snooze:
|
||||
actionSheets = .allSheetsDismissed.copy(\.snooze, to: conversationID)
|
||||
case .more:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,20 +453,9 @@ final class ConversationDetailModel: Sendable, ObservableObject {
|
||||
.markAsUnread(itemsWithIDs: [id], itemType: itemType)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func present(alert: AlertModel, origin: ConversationActionOrigin) {
|
||||
switch origin {
|
||||
case .sheet:
|
||||
actionSheets.alert = alert
|
||||
case .toolbar:
|
||||
actionAlert = alert
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func hideAlert() {
|
||||
actionAlert = nil
|
||||
actionSheets.alert = nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -781,11 +772,11 @@ extension ConversationDetailModel {
|
||||
|
||||
private func onReplyAction(messageId: ID, action: ReplyAction, toastStateStore: ToastStateStore) {
|
||||
Task {
|
||||
await draftPresenter.handleReplyAction(
|
||||
for: messageId, action: action,
|
||||
onError: { error in
|
||||
toastStateStore.present(toast: .error(message: error.localizedDescription))
|
||||
})
|
||||
do {
|
||||
try await draftPresenter.handleReplyAction(for: messageId, action: action)
|
||||
} catch {
|
||||
toastStateStore.present(toast: .error(message: error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,7 +811,7 @@ extension ConversationDetailModel {
|
||||
mailbox: mailbox,
|
||||
conversationId: conversationItem.id
|
||||
).get()
|
||||
self.conversationToolbarActions = .conversation(actions: actions)
|
||||
self.conversationToolbarActions = .conversation(actions: actions, conversationID: conversationItem.id)
|
||||
} catch {
|
||||
AppLogger.log(error: error, category: .conversationDetail)
|
||||
}
|
||||
@@ -835,7 +826,7 @@ extension ConversationDetailModel {
|
||||
theme: theme,
|
||||
messageId: conversationItem.id
|
||||
).get()
|
||||
self.conversationToolbarActions = .message(actions: actions)
|
||||
self.conversationToolbarActions = .message(actions: actions, messageID: conversationItem.id)
|
||||
} catch {
|
||||
AppLogger.log(error: error, category: .conversationDetail)
|
||||
}
|
||||
@@ -907,7 +898,7 @@ enum ConversationModelError: Error {
|
||||
|
||||
private extension MailboxActionSheetsState {
|
||||
static func initial() -> Self {
|
||||
.init(message: nil, labelAs: nil, moveTo: nil)
|
||||
.init(labelAs: nil, moveTo: nil, editToolbar: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,14 +55,17 @@ struct ConversationDetailScreen: View {
|
||||
conversationView
|
||||
.conversationBottomToolbar(
|
||||
actions: model.conversationToolbarActions,
|
||||
mailbox: { model.mailbox.unsafelyUnwrapped },
|
||||
mailUserSession: mailUserSession,
|
||||
messageAppearanceOverrideStore: model.messageAppearanceOverrideStore,
|
||||
editToolbarTapped: { toolbarType in model.actionSheets.editToolbar = toolbarType },
|
||||
messageActionSelected: { action in
|
||||
if let messageID = model.state.singleMessageIDInMessageMode {
|
||||
Task {
|
||||
await model.handle(
|
||||
action: action,
|
||||
messageID: messageID,
|
||||
toastStateStore: toastStateStore,
|
||||
actionOrigin: .toolbar,
|
||||
toastStateStore: toastStateStore
|
||||
) {
|
||||
goBackToMailbox()
|
||||
}
|
||||
@@ -71,7 +74,7 @@ struct ConversationDetailScreen: View {
|
||||
},
|
||||
conversationActionSelected: { action in
|
||||
Task {
|
||||
await model.handle(action: action, toastStateStore: toastStateStore, actionOrigin: .toolbar) {
|
||||
await model.handle(action: action, toastStateStore: toastStateStore) {
|
||||
goBackToMailbox()
|
||||
}
|
||||
}
|
||||
@@ -91,31 +94,16 @@ struct ConversationDetailScreen: View {
|
||||
mailbox: { model.mailbox.unsafelyUnwrapped },
|
||||
mailUserSession: mailUserSession,
|
||||
state: $model.actionSheets,
|
||||
messageActionTapped: { action, id in
|
||||
Task {
|
||||
await model.handle(
|
||||
action: action,
|
||||
messageID: id,
|
||||
toastStateStore: toastStateStore,
|
||||
actionOrigin: .sheet
|
||||
) {
|
||||
goBackToMailbox()
|
||||
}
|
||||
}
|
||||
},
|
||||
conversationActionTapped: { action in
|
||||
Task {
|
||||
await model.handle(
|
||||
action: action,
|
||||
toastStateStore: toastStateStore,
|
||||
actionOrigin: .sheet
|
||||
) {
|
||||
goBackToMailbox()
|
||||
}
|
||||
}
|
||||
},
|
||||
goBackNavigation: { goBackToMailbox() },
|
||||
messageAppearanceOverrideStore: model.messageAppearanceOverrideStore
|
||||
goBackNavigation: { goBackToMailbox() }
|
||||
)
|
||||
.sheet(
|
||||
item: $model.actionSheets.editToolbar,
|
||||
content: { toolbarType in
|
||||
EditToolbarScreen(
|
||||
state: .initial(toolbarType: toolbarType),
|
||||
customizeToolbarService: mailUserSession
|
||||
)
|
||||
}
|
||||
)
|
||||
.alert(model: $model.actionAlert)
|
||||
.fullScreenCover(item: $model.attachmentIDToOpen) { id in
|
||||
@@ -156,6 +144,7 @@ struct ConversationDetailScreen: View {
|
||||
model: model,
|
||||
mailUserSession: mailUserSession,
|
||||
draftPresenter: draftPresenter,
|
||||
editToolbar: {},
|
||||
goBack: { goBackToMailbox() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
import proton_app_uniffi
|
||||
|
||||
enum ConversationToolbarActions {
|
||||
case message(actions: AllMessageActions)
|
||||
case conversation(actions: AllConversationActions)
|
||||
case message(actions: AllMessageActions, messageID: ID)
|
||||
case conversation(actions: AllConversationActions, conversationID: ID)
|
||||
}
|
||||
|
||||
extension ConversationToolbarActions {
|
||||
|
||||
var isEmpty: Bool {
|
||||
switch self {
|
||||
case .message(let actions):
|
||||
case .message(let actions, _):
|
||||
actions.visibleMessageActions.isEmpty
|
||||
case .conversation(let actions):
|
||||
case .conversation(let actions, _):
|
||||
actions.visibleListActions.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,10 @@ import proton_app_uniffi
|
||||
import SwiftUI
|
||||
|
||||
struct MailboxActionSheetsState: Copying {
|
||||
var message: MessageActionsSheetInput?
|
||||
var conversation: ConversationActionsSheetInput?
|
||||
var labelAs: ActionSheetInput?
|
||||
var moveTo: ActionSheetInput?
|
||||
var snooze: ID?
|
||||
var alert: AlertModel?
|
||||
var editToolbar: ToolbarType?
|
||||
}
|
||||
|
||||
extension View {
|
||||
@@ -35,20 +33,14 @@ extension View {
|
||||
mailbox: @escaping () -> Mailbox,
|
||||
mailUserSession: MailUserSession,
|
||||
state: Binding<MailboxActionSheetsState>,
|
||||
messageActionTapped: @escaping (MessageAction, ID) -> Void,
|
||||
conversationActionTapped: @escaping (ConversationAction) -> Void,
|
||||
goBackNavigation: (() -> Void)? = nil,
|
||||
messageAppearanceOverrideStore: MessageAppearanceOverrideStore
|
||||
goBackNavigation: (() -> Void)? = nil
|
||||
) -> some View {
|
||||
modifier(
|
||||
MailboxActionSheets(
|
||||
mailbox: mailbox,
|
||||
mailUserSession: mailUserSession,
|
||||
state: state,
|
||||
messageActionTapped: messageActionTapped,
|
||||
conversationActionTapped: conversationActionTapped,
|
||||
goBackNavigation: goBackNavigation,
|
||||
messageAppearanceOverrideStore: messageAppearanceOverrideStore
|
||||
goBackNavigation: goBackNavigation
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -58,54 +50,21 @@ private struct MailboxActionSheets: ViewModifier {
|
||||
private let mailbox: () -> Mailbox
|
||||
private let mailUserSession: MailUserSession
|
||||
private let goBackNavigation: (() -> Void)?
|
||||
private let messageActionTapped: (MessageAction, ID) -> Void
|
||||
private let conversationActionTapped: (ConversationAction) -> Void
|
||||
private let messageAppearanceOverrideStore: MessageAppearanceOverrideStore
|
||||
|
||||
init(
|
||||
mailbox: @escaping () -> Mailbox,
|
||||
mailUserSession: MailUserSession,
|
||||
state: Binding<MailboxActionSheetsState>,
|
||||
messageActionTapped: @escaping (MessageAction, ID) -> Void,
|
||||
conversationActionTapped: @escaping (ConversationAction) -> Void,
|
||||
goBackNavigation: (() -> Void)?,
|
||||
messageAppearanceOverrideStore: MessageAppearanceOverrideStore
|
||||
goBackNavigation: (() -> Void)?
|
||||
) {
|
||||
self.mailbox = mailbox
|
||||
self.mailUserSession = mailUserSession
|
||||
self._state = state
|
||||
self.conversationActionTapped = conversationActionTapped
|
||||
self.messageActionTapped = messageActionTapped
|
||||
self.goBackNavigation = goBackNavigation
|
||||
self.messageAppearanceOverrideStore = messageAppearanceOverrideStore
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.sheet(item: $state.message) { input in
|
||||
MessageActionsSheet(
|
||||
state: .initial(
|
||||
messageID: input.id,
|
||||
title: input.title,
|
||||
isEditToolbarVisible: input.origin.isEditToolbarVisible
|
||||
),
|
||||
mailbox: mailbox(),
|
||||
mailUserSession: mailUserSession,
|
||||
messageAppearanceOverrideStore: messageAppearanceOverrideStore,
|
||||
actionTapped: { messageActionTapped($0, input.id) }
|
||||
)
|
||||
.alert(model: $state.alert)
|
||||
}
|
||||
.sheet(item: $state.conversation) { input in
|
||||
ConversationActionsSheet(
|
||||
conversationID: input.id,
|
||||
title: input.title,
|
||||
mailbox: mailbox(),
|
||||
mailUserSession: mailUserSession,
|
||||
actionTapped: { conversationActionTapped($0) }
|
||||
)
|
||||
.alert(model: $state.alert)
|
||||
}
|
||||
.sheet(item: snoozeBinding) { conversationID in
|
||||
SnoozeView(
|
||||
state: .initial(
|
||||
|
||||
@@ -234,10 +234,22 @@ struct HomeScreen: View {
|
||||
}
|
||||
|
||||
private func handleDeepLink(_ deepLink: URL) {
|
||||
guard isCurrentSessionActive() else {
|
||||
return
|
||||
}
|
||||
|
||||
if let route = DeepLinkRouteCoder.decode(deepLink: deepLink) {
|
||||
modalState = nil
|
||||
appUIStateStore.toggleSidebar(isOpen: false)
|
||||
appRoute.updateRoute(to: route)
|
||||
}
|
||||
}
|
||||
|
||||
private func isCurrentSessionActive() -> Bool {
|
||||
guard let activeSession = appContext.sessionState.userSession else {
|
||||
return false
|
||||
}
|
||||
|
||||
return ObjectIdentifier(activeSession) == ObjectIdentifier(userSession)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct AvatarInfo: Hashable {
|
||||
let color: Color
|
||||
}
|
||||
|
||||
struct AvatarUIModel: Equatable {
|
||||
struct AvatarUIModel: Hashable {
|
||||
let info: AvatarInfo
|
||||
let type: AvatarViewType
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ final class ReportProblemStateStore: StateStore {
|
||||
private let issueReportBuilder: IssueReportBuilder
|
||||
private let dismiss: () -> Void
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
state: ReportProblemState,
|
||||
reportProblemService: ReportProblemService,
|
||||
@@ -45,7 +44,6 @@ final class ReportProblemStateStore: StateStore {
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handle(action: ReportProblemAction) async {
|
||||
switch action {
|
||||
case .textEntered:
|
||||
@@ -110,12 +108,10 @@ final class ReportProblemStateStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private var isFormEmpty: Bool {
|
||||
[state.summary, state.actualResults, state.expectedResults, state.stepsToReproduce].allSatisfy(\.isEmpty)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private var issueReport: IssueReport {
|
||||
issueReportBuilder.build(
|
||||
with: .init(
|
||||
|
||||
@@ -23,11 +23,17 @@ import SwiftUI
|
||||
struct MobileSignatureScreen: View {
|
||||
@EnvironmentObject private var toastStateStore: ToastStateStore
|
||||
|
||||
let customSettings: CustomSettingsProtocol
|
||||
private let customSettings: CustomSettingsProtocol
|
||||
private let initialState: MobileSignatureState
|
||||
|
||||
init(customSettings: CustomSettingsProtocol, initialState: MobileSignatureState = .initial) {
|
||||
self.customSettings = customSettings
|
||||
self.initialState = initialState
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
StoreView(
|
||||
store: MobileSignatureStateStore(customSettings: customSettings)
|
||||
store: MobileSignatureStateStore(customSettings: customSettings, initialState: initialState)
|
||||
) { state, store in
|
||||
ZStack {
|
||||
DS.Color.BackgroundInverted.norm
|
||||
@@ -85,8 +91,6 @@ struct MobileSignatureScreen: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let toastStateStore = ToastStateStore(initialState: .initial)
|
||||
|
||||
NavigationStack {
|
||||
MobileSignatureScreen(customSettings: CustomSettingsPreviewProvider(status: .enabled))
|
||||
}
|
||||
|
||||
@@ -22,10 +22,6 @@ import proton_app_uniffi
|
||||
struct MobileSignatureState: Copying, Equatable {
|
||||
var mobileSignature: MobileSignature
|
||||
var toast: Toast?
|
||||
|
||||
var textBoxOpacity: Double {
|
||||
mobileSignature.status.isEnabled ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
extension MobileSignatureStatus {
|
||||
|
||||
@@ -20,7 +20,6 @@ import InboxCore
|
||||
import InboxCoreUI
|
||||
import proton_app_uniffi
|
||||
|
||||
@MainActor
|
||||
final class MobileSignatureStateStore: StateStore {
|
||||
@Published var state: MobileSignatureState = .initial
|
||||
|
||||
@@ -33,9 +32,14 @@ final class MobileSignatureStateStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
init(customSettings: CustomSettingsProtocol, clock: any Clock<Duration> = ContinuousClock()) {
|
||||
init(
|
||||
customSettings: CustomSettingsProtocol,
|
||||
clock: any Clock<Duration> = ContinuousClock(),
|
||||
initialState: MobileSignatureState = .initial
|
||||
) {
|
||||
self.customSettings = customSettings
|
||||
self.clock = clock
|
||||
state = initialState
|
||||
}
|
||||
|
||||
func handle(action: MobileSignatureStateStoreAction) async {
|
||||
|
||||
@@ -97,7 +97,7 @@ struct SidebarScreen: View {
|
||||
separator
|
||||
}.background(
|
||||
GeometryReader { geometry in
|
||||
TransparentBlur()
|
||||
BlurredBackground(fallbackBackgroundColor: DS.Color.Sidebar.background)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.preference(key: HeightPreferenceKey.self, value: geometry.size.height)
|
||||
.onPreferenceChange(HeightPreferenceKey.self) { value in
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import InboxDesignSystem
|
||||
import SwiftUI
|
||||
|
||||
enum AvatarViewType: Equatable {
|
||||
enum AvatarViewType: Hashable {
|
||||
case sender(params: SenderImageDataParameters)
|
||||
case other
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
// Copyright (c) 2024 Proton Technologies AG
|
||||
//
|
||||
// This file is part of Proton Mail.
|
||||
//
|
||||
// Proton Mail 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
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import InboxDesignSystem
|
||||
import SwiftUI
|
||||
|
||||
struct CapsuleView: View {
|
||||
let text: LocalizedStringResource
|
||||
let color: Color
|
||||
let icon: Image?
|
||||
let iconColor: Color?
|
||||
let style: CapsuleStyle
|
||||
|
||||
init(
|
||||
text: LocalizedStringResource,
|
||||
color: Color,
|
||||
icon: Image? = nil,
|
||||
iconColor: Color? = nil,
|
||||
style: CapsuleStyle
|
||||
) {
|
||||
self.text = text
|
||||
self.color = color
|
||||
self.icon = icon
|
||||
self.iconColor = iconColor
|
||||
self.style = style
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: DS.Spacing.small) {
|
||||
if let icon {
|
||||
icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.square(size: 14)
|
||||
.foregroundColor(iconColor ?? DS.Color.Icon.weak)
|
||||
}
|
||||
Text(verbatim: text.string)
|
||||
.font(.caption2)
|
||||
.fontWeight(style.fontWeight)
|
||||
.foregroundColor(style.fontColor)
|
||||
.lineLimit(1)
|
||||
.frame(minWidth: 30)
|
||||
|
||||
}
|
||||
.padding(EdgeInsets(top: DS.Spacing.small, leading: DS.Spacing.standard, bottom: DS.Spacing.small, trailing: DS.Spacing.standard))
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: DS.Radius.medium)
|
||||
.foregroundColor(color)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct CapsuleStyle {
|
||||
let fontColor: Color
|
||||
let fontWeight: Font.Weight
|
||||
|
||||
static let attachment: CapsuleStyle = {
|
||||
.init(fontColor: DS.Color.Text.norm, fontWeight: .regular)
|
||||
}()
|
||||
|
||||
static let label: CapsuleStyle = {
|
||||
.init(fontColor: .white, fontWeight: .semibold)
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack {
|
||||
CapsuleView(text: "".notLocalized.stringResource, color: DS.Color.Background.secondary, style: .attachment)
|
||||
CapsuleView(text: "2 files".notLocalized.stringResource, color: DS.Color.Background.secondary, icon: Image(DS.Icon.icPaperClip), style: .attachment)
|
||||
CapsuleView(text: "games".notLocalized.stringResource, color: DS.Color.Background.secondary, icon: Image(systemName: "gamecontroller"), style: .attachment)
|
||||
CapsuleView(text: "Work".notLocalized.stringResource, color: .blue, style: .label)
|
||||
CapsuleView(text: "Friends & Fam".notLocalized.stringResource, color: .pink, style: .label)
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,6 @@ final class EmptyFolderBannerStateStore: StateStore {
|
||||
|
||||
// MARK: - StateStore
|
||||
|
||||
@MainActor
|
||||
func handle(action: Action) async {
|
||||
switch action {
|
||||
case .upgradeToAutoDelete:
|
||||
|
||||
@@ -61,7 +61,7 @@ struct MailboxItemsListView<EmptyView: View>: View {
|
||||
.toolbar(selectionState.hasItems ? .visible : .hidden, for: .bottomBar)
|
||||
.animation(.default, value: selectionState.hasItems)
|
||||
.listActionsToolbar(
|
||||
state: .initial,
|
||||
initialState: .initial,
|
||||
availableActions: .productionInstance,
|
||||
itemTypeForActionBar: config.itemTypeForActionBar,
|
||||
mailUserSession: mailUserSession,
|
||||
|
||||
@@ -24,6 +24,7 @@ import SwiftUI
|
||||
struct MessageAddressActionView: View {
|
||||
@EnvironmentObject var toastStateStore: ToastStateStore
|
||||
@Environment(\.openURL) var openURL
|
||||
@Environment(\.pasteboard) var pasteboard
|
||||
@Environment(\.dismissTestable) var dismiss
|
||||
let avatarUIModel: AvatarUIModel
|
||||
let name: String
|
||||
@@ -40,7 +41,7 @@ struct MessageAddressActionView: View {
|
||||
phoneNumber: .none,
|
||||
session: mailUserSession,
|
||||
toastStateStore: toastStateStore,
|
||||
pasteboard: .general,
|
||||
pasteboard: pasteboard,
|
||||
openURL: openURL,
|
||||
blockAddress: blockAddress(session:email:),
|
||||
draftPresenter: draftPresenter,
|
||||
|
||||
@@ -70,7 +70,6 @@ final class MessageAddressActionViewStateStore: StateStore {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
@MainActor
|
||||
func handle(action: Action) async {
|
||||
switch action {
|
||||
case .onTap(let tapAction):
|
||||
@@ -82,7 +81,6 @@ final class MessageAddressActionViewStateStore: StateStore {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@MainActor
|
||||
private func handleTap(action: MessageAddressAction) async {
|
||||
switch action {
|
||||
case .newMessage:
|
||||
@@ -97,9 +95,9 @@ final class MessageAddressActionViewStateStore: StateStore {
|
||||
openURL(url)
|
||||
}
|
||||
case .copyAddress:
|
||||
clipboard.copyToClipboard(value: state.email, forName: L10n.Action.Clipboard.emailAddress)
|
||||
clipboard.copyToClipboard(value: state.email, forName: CommonL10n.Clipboard.emailAddress)
|
||||
case .copyName:
|
||||
clipboard.copyToClipboard(value: state.name, forName: L10n.Action.Clipboard.name)
|
||||
clipboard.copyToClipboard(value: state.name, forName: CommonL10n.Clipboard.name)
|
||||
case .addToContacts:
|
||||
toastStateStore.present(toast: .comingSoon)
|
||||
case .blockContact:
|
||||
@@ -107,7 +105,6 @@ final class MessageAddressActionViewStateStore: StateStore {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func handleAlert(action: BlockAddressAlertAction) async {
|
||||
switch action {
|
||||
case .cancel:
|
||||
|
||||
@@ -35,7 +35,6 @@ struct MessageBannersView: View {
|
||||
case unsubscribeNewsletterTapped
|
||||
}
|
||||
|
||||
@EnvironmentObject var toastStateStore: ToastStateStore
|
||||
let types: OrderedSet<MessageBanner>
|
||||
let timerPublisher: Publishers.Autoconnect<Timer.TimerPublisher>
|
||||
let scheduleSendDateFormatter: ScheduleSendDateFormatter
|
||||
|
||||
@@ -137,7 +137,7 @@ class RecurringBackgroundTaskScheduler: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
private func checkForSessionSetUpToComplete(completion: @Sendable @escaping () -> Void) {
|
||||
private func checkForSessionSetUpToComplete(completion: @escaping () -> Void) {
|
||||
sessionSetUpCheckCancellable =
|
||||
Publishers
|
||||
.CombineLatest(sessionStatePublisher, timerFactory(0.5))
|
||||
|
||||
@@ -48,17 +48,6 @@ enum L10n {
|
||||
)
|
||||
}
|
||||
|
||||
enum Clipboard {
|
||||
static let emailAddress = LocalizedStringResource(
|
||||
"email address",
|
||||
comment: "Label for an email address when copying it to the clipboard."
|
||||
)
|
||||
static let name = LocalizedStringResource(
|
||||
"name",
|
||||
comment: "Label for a person's name when copying it to the clipboard."
|
||||
)
|
||||
}
|
||||
|
||||
enum Print {
|
||||
static let error = LocalizedStringResource(
|
||||
"Could not print requested e-mail",
|
||||
@@ -214,6 +203,10 @@ enum L10n {
|
||||
"Reply all",
|
||||
comment: "Action title for replying to a sender and all receipients of given message in the action sheet."
|
||||
)
|
||||
static let moreOptions = LocalizedStringResource(
|
||||
"More options",
|
||||
comment: "A button title that shows rest of available actions in the menu"
|
||||
)
|
||||
|
||||
enum Delete {
|
||||
enum Alert {
|
||||
@@ -879,6 +872,29 @@ enum L10n {
|
||||
}
|
||||
|
||||
enum Settings {
|
||||
enum AppIcon {
|
||||
static let buttonTitle = LocalizedStringResource(
|
||||
"App Icon",
|
||||
comment: "Title of the button that allows the user to change the app’s icon."
|
||||
)
|
||||
static let primary = LocalizedStringResource(
|
||||
"Primary",
|
||||
comment: "Name of the default (primary) app icon shown in the app icon picker."
|
||||
)
|
||||
static let notes = LocalizedStringResource(
|
||||
"Notes",
|
||||
comment: "Name of the alternate 'Notes' app icon shown in the app icon picker."
|
||||
)
|
||||
static let weather = LocalizedStringResource(
|
||||
"Weather",
|
||||
comment: "Name of the alternate 'Weather' app icon shown in the app icon picker."
|
||||
)
|
||||
static let calculator = LocalizedStringResource(
|
||||
"Calculator",
|
||||
comment: "Name of the alternate 'Calculator' app icon shown in the app icon picker."
|
||||
)
|
||||
}
|
||||
|
||||
enum App {
|
||||
static let title = LocalizedStringResource(
|
||||
"App customizations",
|
||||
@@ -1043,15 +1059,19 @@ enum L10n {
|
||||
comment: "Title of a section displaying selected list toolbar actions."
|
||||
)
|
||||
static let listToolbarSectionFooter = LocalizedStringResource(
|
||||
"This toolbar appears when multiple messages are selected in the message list (e.g., Inbox, Trash, etc.).",
|
||||
"This toolbar appears when multiple messages are selected in a list view.",
|
||||
comment: "Footer of a section displaying selected list toolbar actions."
|
||||
)
|
||||
static let messageToolbarSectionTitle = LocalizedStringResource(
|
||||
"Message toolbar",
|
||||
comment: "Title of a section displaying selected message toolbar actions."
|
||||
)
|
||||
static let conversationToolbarSectionTitle = LocalizedStringResource(
|
||||
"Conversation toolbar",
|
||||
comment: "Title of a section displaying selected conversation toolbar actions."
|
||||
)
|
||||
static let conversationToolbarSectionFooter = LocalizedStringResource(
|
||||
"This toolbar remains consistently visible during message reading.",
|
||||
"This toolbar remains visible when a message is open.",
|
||||
comment: "Footer of a section displaying selected conversation toolbar actions."
|
||||
)
|
||||
static let editActions = LocalizedStringResource(
|
||||
|
||||
@@ -70,6 +70,7 @@ extension Message {
|
||||
id: id,
|
||||
unread: unread,
|
||||
messageDetails: MessageDetailsUIModel(
|
||||
id: id,
|
||||
avatar: sender.senderAvatar,
|
||||
sender: .init(
|
||||
name: sender.uiRepresentation,
|
||||
|
||||
@@ -15,22 +15,22 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
struct MessageActionsSheetInput: Identifiable {
|
||||
enum Origin {
|
||||
case toolbar
|
||||
case messageHeader
|
||||
@testable import ProtonMail
|
||||
|
||||
var isEditToolbarVisible: Bool {
|
||||
switch self {
|
||||
case .toolbar:
|
||||
true
|
||||
case .messageHeader:
|
||||
false
|
||||
}
|
||||
}
|
||||
class AppIconConfiguratorSpy: AppIconConfigurable {
|
||||
var stubbedSupportsAlternateIcons: Bool = true
|
||||
|
||||
private(set) var setAlternateIconNameCalls: [String?] = []
|
||||
|
||||
// MARK: - AppIconConfigurable
|
||||
|
||||
var alternateIconName: String?
|
||||
|
||||
var supportsAlternateIcons: Bool {
|
||||
stubbedSupportsAlternateIcons
|
||||
}
|
||||
|
||||
let id: ID
|
||||
let title: String
|
||||
let origin: Origin
|
||||
func setAlternateIconName(_ alternateIconName: String?) async throws {
|
||||
setAlternateIconNameCalls.append(alternateIconName)
|
||||
}
|
||||
}
|
||||