Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0767bf9
Add hide topic endpoint
Xialtal Apr 7, 2026
f8c2c2c
Add undo option to topic posts delete endpoint
Xialtal Apr 7, 2026
b6c3361
Add hide option to topic context menu
Xialtal Apr 7, 2026
2eff094
Add close topic endpoint
Xialtal Apr 7, 2026
71c8778
Add close option to topic context menu
Xialtal Apr 7, 2026
060aaec
Add topic delete endpoint
Xialtal Apr 7, 2026
d67d3bc
Add delete option to topic context menu
Xialtal Apr 7, 2026
83b11ab
Add move topic endpoint
Xialtal Apr 7, 2026
595edc3
Add confirmation for topic delete
Xialtal Apr 7, 2026
8f080b3
Fix topic close option in context menu
Xialtal Apr 7, 2026
97fa90d
Extract CheckBoxToggleStyle to SharedUI
Xialtal Apr 7, 2026
6b44b71
Add set topic curator endpoint
Xialtal Apr 7, 2026
a0f126c
Improve auth check for post context menu
Xialtal Apr 8, 2026
ee9a210
[WIP] Topic & Post tools
Xialtal Apr 8, 2026
0dcf2a2
Improve Post model flag mock
Xialtal Apr 8, 2026
b72bec9
Add topic post restore context menu option
Xialtal Apr 8, 2026
0ae07f3
Improve flag in TopicInfo mocks
Xialtal Apr 8, 2026
22e8221
Fix ForumScreen preview
Xialtal Apr 8, 2026
c17d6df
Add tools action to topic context menu in ForumFeature
Xialtal Apr 8, 2026
63c2c43
Bump API version
Xialtal Apr 8, 2026
6466cab
Add redAlpha color to SharedUI
Xialtal Apr 9, 2026
0832bd0
Improve ForumFlag model
Xialtal Apr 9, 2026
591c300
Add post status to PostRowView
Xialtal Apr 10, 2026
9d222c1
Add post karma history endpoint
Xialtal Apr 10, 2026
65f26d3
Improve karma value for Post model mock
Xialtal Apr 11, 2026
54e6ade
Fix karma field in Post model
Xialtal Apr 11, 2026
60b6713
Add post karma history
Xialtal Apr 11, 2026
b16aeab
Fix user profile not opening from post karma history
Xialtal Apr 11, 2026
9b47bda
Add "last edit hidden" tag support for posts
Xialtal Apr 11, 2026
8f76e12
Add move posts endpoint
Xialtal Apr 11, 2026
977c188
[WIP] ForumMoveFeature
Xialtal Apr 12, 2026
632047f
Add move option to post context menu
Xialtal Apr 12, 2026
12d7d05
Add move option to topic context menu
Xialtal Apr 12, 2026
e8657f6
Add user note endpoint
Xialtal Apr 12, 2026
fb5fa16
Improve User model mock
Xialtal Apr 12, 2026
66eb1e0
Add note support to FormFeature
Xialtal Apr 12, 2026
8efba5c
Allow avatar upload only for session user
Xialtal Apr 12, 2026
ed837e5
Add context menu to user profile
Xialtal Apr 12, 2026
6277e9d
Fix isLastEditHidden check in Post model
Xialtal Apr 12, 2026
b3d0731
Use icon if needed for segment picker in ProfileScreen
Xialtal Apr 12, 2026
eda43c4
Profile logging section improvements
Xialtal Apr 12, 2026
466ba12
Improve ru localization for ForumFeature
Xialtal Apr 13, 2026
e2b9fb6
ForumMoveFeature improvements
Xialtal Apr 13, 2026
d07252c
Improve karma field in Post model
Xialtal Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions Modules/Sources/APIClient/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public struct APIClient: Sendable {
// User
public var getUser: @Sendable (_ userId: Int, _ policy: CachePolicy) async throws -> AsyncThrowingStream<User, any Error>
public var editUserProfile: @Sendable (_ request: UserProfileEditRequest) async throws -> Bool
public var addUserNote: @Sendable (_ userId: Int, _ content: String) async throws -> UserNoteResponse
Comment thread
SubvertDev marked this conversation as resolved.
public var getReputationVotes: @Sendable (_ data: ReputationVotesRequest) async throws -> ReputationVotes
public var changeReputation: @Sendable (_ data: ReputationChangeRequest) async throws -> ReputationChangeResponseType
public var updateUserAvatar: @Sendable (_ userId: Int, _ image: Data) async throws -> UserAvatarResponseType
Expand All @@ -62,7 +63,10 @@ public struct APIClient: Sendable {
public var markRead: @Sendable (_ id: Int, _ isTopic: Bool) async throws -> Bool
public var getAnnouncement: @Sendable (_ id: Int) async throws -> Announcement
public var getTopic: @Sendable (_ id: Int, _ page: Int, _ perPage: Int, _ postsFilter: TopicPostsFilter) async throws -> Topic
public var modifyForum: @Sendable (_ ids: [Int], _ type: ForumModifyType, _ isUndo: Bool) async throws -> Bool
public var moveTopic: @Sendable (_ id: Int, _ toForumId: Int, _ saveLink: Bool) async throws -> Bool
public var getTopicViewers: @Sendable (_ id: Int) async throws -> TopicViewers
public var setTopicCurator: @Sendable (_ topicId: Int, _ userId: Int, _ reason: String) async throws -> Bool
public var getTemplate: @Sendable (_ request: ForumTemplateRequest, _ isTopic: Bool) async throws -> [FormFieldType]
public var sendTemplate: @Sendable (_ id: Int, _ content: PDAPIDocument, _ isTopic: Bool) async throws -> TemplateSend
public var getHistory: @Sendable (_ offset: Int, _ perPage: Int) async throws -> History
Expand All @@ -71,8 +75,9 @@ public struct APIClient: Sendable {
public var previewTemplate: @Sendable (_ id: Int, _ content: PDAPIDocument, _ isTopic: Bool) async throws -> PreviewResponse
public var sendPost: @Sendable (_ request: PostRequest) async throws -> PostSendResponse
public var editPost: @Sendable (_ request: PostEditRequest) async throws -> PostSendResponse
public var deletePosts: @Sendable (_ postIds: [Int]) async throws -> Bool
public var movePosts: @Sendable (_ ids: [Int], _ toTopicId: Int) async throws -> Bool
public var postKarma: @Sendable (_ postId: Int, _ isUp: Bool) async throws -> Bool
public var postKarmaHistory: @Sendable (_ postId: Int) async throws -> [PostKarmaVote]
public var voteInTopicPoll: @Sendable (_ topicId: Int, _ selections: [[Int]]) async throws -> Bool

// Favorites
Expand Down Expand Up @@ -233,6 +238,12 @@ extension APIClient: DependencyKey {
let status = Int(response.getResponseStatus())!
return status == 0
},
addUserNote: { userId, message in
let command = MemberCommand.notice(memberId: userId, message: message)
let response = try await api.send(command)
let status = Int(response.getResponseStatus())!
return UserNoteResponse(rawValue: status)
},
getReputationVotes: { request in
let command = MemberCommand.reputationVotes(data: MemberReputationVotesRequest(
memberId: request.userId,
Expand Down Expand Up @@ -350,6 +361,26 @@ extension APIClient: DependencyKey {
let response = try await api.send(ForumCommand.Topic.view(data: request))
return try await parser.parseTopic(response)
},
modifyForum: { ids, type, isUndo in
let command = ForumCommand.modify(
ids: ids,
type: type.transfer,
isUndo: isUndo
)
let response = try await api.send(command)
let status = Int(response.getResponseStatus())!
return status == 0
},
moveTopic: { id, toForumId, saveLink in
let command = ForumCommand.Topic.move(
id: id,
toForumId: toForumId,
saveLink: saveLink
)
let response = try await api.send(command)
let status = Int(response.getResponseStatus())!
return status == 0
},
getTopicViewers: { topicId in
let command = MemberCommand.sessions(
pageType: .topic,
Expand All @@ -358,6 +389,16 @@ extension APIClient: DependencyKey {
let response = try await api.send(command)
return try await parser.parseTopicViewers(response)
},
setTopicCurator: { topicId, userId, reason in
let command = ForumCommand.Topic.setCurator(
topicId: topicId,
memberId: userId,
reason: reason
)
let response = try await api.send(command)
let status = Int(response.getResponseStatus())!
return status == 0
},

getTemplate: { request, isTopic in
let command = ForumCommand.template(
Expand Down Expand Up @@ -431,8 +472,8 @@ extension APIClient: DependencyKey {
return try await parser.parsePostSendResponse(response)
},

deletePosts: { ids in
let command = ForumCommand.Post.delete(postIds: ids)
movePosts: { ids, toTopicId in
Comment thread
Xialtal marked this conversation as resolved.
let command = ForumCommand.Post.move(ids: ids, toTopicId: toTopicId)
let response = try await api.send(command)
let status = Int(response.getResponseStatus())!
return status == 0
Expand All @@ -447,6 +488,11 @@ extension APIClient: DependencyKey {
let status = Int(response.getResponseStatus())!
return status == 0
},
postKarmaHistory: { postId in
let command = ForumCommand.Post.history(id: postId)
let response = try await api.send(command)
return try await parser.parsePostKarmaHistory(response)
},

voteInTopicPoll: { topicId, selections in
let command = ForumCommand.Topic.Poll.vote(topicId: topicId, selections: selections)
Expand Down Expand Up @@ -633,6 +679,9 @@ extension APIClient: DependencyKey {
editUserProfile: { _ in
return true
},
addUserNote: { _, _ in
return .success
},
getReputationVotes: { _ in
return .mock
},
Expand All @@ -652,7 +701,9 @@ extension APIClient: DependencyKey {
return .finished()
},
getForum: { _, _, _, _ in
return .finished()
let (stream, continuation) = AsyncThrowingStream.makeStream(of: Forum.self)
continuation.yield(with: .success(.mock))
return stream
},
getForumStat: { _ in
return .mock
Expand All @@ -669,9 +720,18 @@ extension APIClient: DependencyKey {
getTopic: { _, _, _, _ in
return .mock
},
modifyForum: { _, _, _ in
return true
},
moveTopic: { _, _, _ in
return true
},
getTopicViewers: { _ in
return .mock
},
setTopicCurator: { _, _, _ in
return true
},
getTemplate: { _, _ in
return [.mockTitle, .mockRequiredText, .mockRequiredEditor, .mockEditor, .mockUploadBox]
},
Expand All @@ -696,12 +756,15 @@ extension APIClient: DependencyKey {
editPost: { _ in
return .success(PostSend(id: 0, topicId: 1, offset: 2))
},
deletePosts: { _ in
movePosts: { _, _ in
return true
},
postKarma: { _, _ in
return true
},
postKarmaHistory: { _ in
return .mock
},
voteInTopicPoll: { _, _ in
return true
},
Expand Down
47 changes: 47 additions & 0 deletions Modules/Sources/APIClient/Models/ForumModifyType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// ForumModifyType.swift
// ForPDA
//
// Created by Xialtal on 8.04.26.
//

import PDAPI
import Models

public enum ForumModifyType: Sendable {
case post(PostModifyAction)
case topic(TopicModifyAction)
}

extension ForumModifyType {
var transfer: ForumCommand.ModifyType {
switch self {
case .post(let action):
.post(action: action.transfer)
case .topic(let action):
.topic(action: action.transfer)
}
}
}

fileprivate extension PostModifyAction {
var transfer: ForumCommand.ModifyPostAction {
switch self {
case .pin: .pin
case .hide: .hide
case .delete: .delete
case .protect: .protect
}
}
}

fileprivate extension TopicModifyAction {
var transfer: ForumCommand.ModifyTopicAction {
switch self {
case .pin: .pin
case .hide: .hide
case .close: .close
case .delete: .delete
}
}
}
20 changes: 20 additions & 0 deletions Modules/Sources/FormFeature/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@
}
}
},
"New note" : {
"localizations" : {
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Новая заметка"
}
}
}
},
"New post" : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -119,6 +129,16 @@
}
}
},
"Not set reason for note" : {
"localizations" : {
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Не указана причина"
}
}
}
},
"OK" : {
"localizations" : {
"ru" : {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import SwiftUI
import ComposableArchitecture
import Models
import SharedUI

// MARK: - Feature

Expand Down Expand Up @@ -113,7 +114,7 @@ struct FormCheckBoxListRow: View {
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
}
.toggleStyle(CheckBox())
.toggleStyle(CheckBoxToggleStyle())
.padding(6)
}
}
Expand Down
42 changes: 40 additions & 2 deletions Modules/Sources/FormFeature/Sources/FormFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public struct FormFeature: Reducer, Sendable {
case loadForm(id: Int, isTopic: Bool)
case formResponse(Result<[FormFieldType], any Error>)
case reportResponse(Result<ReportResponseType, any Error>)
case noteResponse(Result<UserNoteResponse, any Error>)
case simplePostResponse(Result<PostSendResponse, any Error>)
case templateResponse(Result<TemplateSend, any Error>)
case publishForm(flag: PostSendFlag)
Expand Down Expand Up @@ -234,7 +235,7 @@ public struct FormFeature: Reducer, Sendable {
state.isFormLoading = true
return .send(.internal(.loadForm(id: forumId, isTopic: true)))

case .report:
case .report, .note:
let editorState = FormEditorFeature.State(id: 0, flag: .required, uploadBox: nil)
state.rows.append(.editor(editorState))
state.focusedField = 0
Expand Down Expand Up @@ -265,7 +266,7 @@ public struct FormFeature: Reducer, Sendable {
formType: .post(type: type, topicId: topicId, content: content)
)

case .report:
case .report, .note:
let content = if case let .string(text) = state.content.first { text } else {
fatalError("Report content field should contains only one .string()!")
}
Expand Down Expand Up @@ -441,10 +442,37 @@ public struct FormFeature: Reducer, Sendable {
await send(.internal(.reportResponse(result)))
}

case let .note(userId: userId):
let content = if case let .string(text) = state.content.first { text } else {
fatalError("Bad note content: \(state.content)")
}
return .run { [content = content] send in
let result = await Result { try await apiClient.addUserNote(
userId: userId,
content: content
) }
await send(.internal(.noteResponse(result)))
}

default:
fatalError()
}

case let .internal(.noteResponse(.success(result))):
switch result {
case .error:
state.destination = .alert(.unknownError)
case .reasonNotSet:
state.destination = .alert(.noteWithoutReason)
case .success:
return .send(.delegate(.formSent(.note)))
}

case let .internal(.noteResponse(.failure(error))):
state.isPublishing = false
state.destination = .alert(.unknownError)
analyticsClient.capture(error)

case let .internal(.reportResponse(.success(result))):
switch result {
case .error:
Expand Down Expand Up @@ -613,6 +641,16 @@ public extension AlertState where Action == FormFeature.Destination.Alert {
}
}

// Note

nonisolated(unsafe) static let noteWithoutReason = AlertState {
TextState("Not set reason for note")
} actions: {
ButtonState {
TextState("OK")
}
}

// Common

nonisolated(unsafe) static let unknownError = AlertState {
Expand Down
1 change: 1 addition & 0 deletions Modules/Sources/FormFeature/Sources/FormScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public struct FormScreen: View {
}
case .topic: "New topic"
case .report: "Send report"
case .note: "New note"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public struct FormPreviewFeature: Reducer, Sendable {
return .send(.internal(.loadPreview(id: topicId, content: content)))
}

case .report(_, _):
case .report, .note:
// handling as .post
break
}
Expand Down
1 change: 1 addition & 0 deletions Modules/Sources/FormFeature/Sources/Support/FormType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum FormType: Sendable, Equatable {
case post(type: PostType, topicId: Int, content: PostContentType)
case report(id: Int, type: ReportType)
case topic(forumId: Int, content: [FormValue])
case note(userId: Int)

public enum PostType: Sendable, Equatable {
case new
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct EditReasonView: View {
.foregroundStyle(Color(.Labels.secondary))
.frame(maxWidth: .infinity, alignment: .leading)
}
.toggleStyle(CheckBox())
.toggleStyle(CheckBoxToggleStyle())
.tint(tintColor)
.padding(6)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ extension ForumFeature {
break // TODO: Add
}

case .view(.contextTopicToolsMenu):
// MARK: Moderator tools are skip analytics
break

case let .view(.contextTopicMenu(option, topic)):
switch option {
case .open:
Expand Down
Loading
Loading