feat: implement reply status entry and update query of API
This commit is contained in:
parent
0eff43e1d1
commit
d5c9473528
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D75" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
|
@ -115,6 +115,7 @@
|
|||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="url" attributeType="String"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="status" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="mentions" inverseEntity="Status"/>
|
||||
|
@ -209,7 +210,7 @@
|
|||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
||||
<element name="Mention" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||
|
|
|
@ -10,6 +10,9 @@ import Foundation
|
|||
|
||||
public final class Mention: NSManagedObject {
|
||||
public typealias ID = UUID
|
||||
|
||||
@NSManaged public private(set) var index: NSNumber
|
||||
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@NSManaged public private(set) var id: String
|
||||
@NSManaged public private(set) var createAt: Date
|
||||
|
@ -32,9 +35,11 @@ public extension Mention {
|
|||
@discardableResult
|
||||
static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
property: Property,
|
||||
index: Int
|
||||
) -> Mention {
|
||||
let mention: Mention = context.insertObject()
|
||||
mention.index = NSNumber(value: index)
|
||||
mention.id = property.id
|
||||
mention.username = property.username
|
||||
mention.acct = property.acct
|
||||
|
|
|
@ -57,7 +57,19 @@ extension ComposeStatusSection {
|
|||
return
|
||||
}
|
||||
let status = replyTo.reblog ?? replyTo
|
||||
|
||||
// set avatar
|
||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
|
||||
// set name username
|
||||
cell.statusView.nameLabel.text = {
|
||||
let author = status.author
|
||||
return author.displayName.isEmpty ? author.username : author.displayName
|
||||
}()
|
||||
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
|
||||
// set text
|
||||
cell.statusView.activeTextLabel.configure(content: status.content)
|
||||
// set date
|
||||
cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow
|
||||
}
|
||||
return cell
|
||||
case .input(let replyToStatusObjectID, let attribute):
|
||||
|
|
|
@ -11,7 +11,7 @@ import ActiveLabel
|
|||
enum MastodonField {
|
||||
|
||||
static func parse(field string: String) -> ParseResult {
|
||||
let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.]+)?)")
|
||||
let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?)")
|
||||
let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))")
|
||||
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
||||
|
||||
|
|
|
@ -33,6 +33,10 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
// MARK: - ActionToolbarContainerDelegate
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, replyButtonDidPressed sender: UIButton) {
|
||||
StatusProviderFacade.responseToStatusReplyAction(provider: self, cell: cell)
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) {
|
||||
StatusProviderFacade.responseToStatusReblogAction(provider: self, cell: cell)
|
||||
}
|
||||
|
|
|
@ -278,7 +278,6 @@ extension StatusProviderFacade {
|
|||
|
||||
extension StatusProviderFacade {
|
||||
|
||||
|
||||
static func responseToStatusReblogAction(provider: StatusProvider) {
|
||||
_responseToStatusReblogAction(
|
||||
provider: provider,
|
||||
|
@ -385,6 +384,37 @@ extension StatusProviderFacade {
|
|||
|
||||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
|
||||
static func responseToStatusReplyAction(provider: StatusProvider) {
|
||||
_responseToStatusReplyAction(
|
||||
provider: provider,
|
||||
status: provider.status()
|
||||
)
|
||||
}
|
||||
|
||||
static func responseToStatusReplyAction(provider: StatusProvider, cell: UITableViewCell) {
|
||||
_responseToStatusReplyAction(
|
||||
provider: provider,
|
||||
status: provider.status(for: cell, indexPath: nil)
|
||||
)
|
||||
}
|
||||
|
||||
private static func _responseToStatusReplyAction(provider: StatusProvider, status: Future<Status?, Never>) {
|
||||
status
|
||||
.sink { [weak provider] status in
|
||||
guard let provider = provider else { return }
|
||||
guard let status = status?.reblog ?? status else { return }
|
||||
|
||||
let composeViewModel = ComposeViewModel(context: provider.context, composeKind: .reply(repliedToStatusObjectID: status.objectID))
|
||||
provider.coordinator.present(scene: .compose(viewModel: composeViewModel), from: provider, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
.store(in: &provider.context.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
enum Target {
|
||||
case primary // original status
|
||||
|
|
|
@ -10,7 +10,7 @@ import AVKit
|
|||
|
||||
// Check List Last Updated
|
||||
// - HomeViewController: 2021/4/13
|
||||
// - FavoriteViewController: 2021/4/8
|
||||
// - FavoriteViewController: 2021/4/14
|
||||
// - HashtagTimelineViewController: 2021/4/8
|
||||
// - UserTimelineViewController: 2021/4/13
|
||||
// - ThreadViewController: 2021/4/13
|
||||
|
|
|
@ -548,7 +548,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
|||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
|
||||
|
||||
let stringRange = NSRange(location: 0, length: string.length)
|
||||
let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.]+)?|#([^\\s.]+))")
|
||||
let highlightMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))")
|
||||
// accept ^\B: or \s: but not accept \B: to force user input a space to make emoji take effect
|
||||
// precondition :\B with following space
|
||||
let emojiMatches = string.matches(pattern: "(?:(^\\B:|\\s:)([a-zA-Z0-9_]+)(:\\B(?=\\s)))")
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
||||
|
@ -64,6 +65,15 @@ extension ComposeViewModel.PublishState {
|
|||
guard viewModel.isPollComposing.value else { return nil }
|
||||
return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
|
||||
}()
|
||||
let inReplyToID: Mastodon.Entity.Status.ID? = {
|
||||
guard case let .reply(repliedToStatusObjectID) = viewModel.composeKind else { return nil }
|
||||
var id: Mastodon.Entity.Status.ID?
|
||||
viewModel.context.managedObjectContext.performAndWait {
|
||||
guard let replyTo = viewModel.context.managedObjectContext.object(with: repliedToStatusObjectID) as? Status else { return }
|
||||
id = replyTo.id
|
||||
}
|
||||
return id
|
||||
}()
|
||||
let sensitive: Bool = viewModel.isContentWarningComposing.value
|
||||
let spoilerText: String? = {
|
||||
let text = viewModel.composeStatusAttribute.contentWarningContent.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
@ -105,6 +115,7 @@ extension ComposeViewModel.PublishState {
|
|||
mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
|
||||
pollOptions: pollOptions,
|
||||
pollExpiresIn: pollExpiresIn,
|
||||
inReplyToID: inReplyToID,
|
||||
sensitive: sensitive,
|
||||
spoilerText: spoilerText,
|
||||
visibility: visibility
|
||||
|
|
|
@ -87,7 +87,36 @@ final class ComposeViewModel {
|
|||
self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value)
|
||||
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
|
||||
// end init
|
||||
if case let .hashtag(text) = composeKind {
|
||||
if case let .reply(repliedToStatusObjectID) = composeKind {
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = context.managedObjectContext.object(with: repliedToStatusObjectID) as? Status else { return }
|
||||
let composeAuthor: MastodonUser? = {
|
||||
guard let objectID = self.activeAuthentication.value?.user.objectID else { return nil }
|
||||
guard let author = context.managedObjectContext.object(with: objectID) as? MastodonUser else { return nil }
|
||||
return author
|
||||
}()
|
||||
|
||||
var mentionAccts: [String] = []
|
||||
if composeAuthor?.id != status.author.id {
|
||||
mentionAccts.append("@" + status.author.acct)
|
||||
}
|
||||
let mentions = (status.mentions ?? Set())
|
||||
.sorted(by: { $0.index.intValue < $1.index.intValue })
|
||||
.filter { $0.id != composeAuthor?.id }
|
||||
for mention in mentions {
|
||||
mentionAccts.append("@" + mention.acct)
|
||||
}
|
||||
for acct in mentionAccts {
|
||||
UITextChecker.learnWord(acct)
|
||||
}
|
||||
|
||||
let initialComposeContent = mentionAccts.joined(separator: " ")
|
||||
let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
|
||||
self.preInsertedContent = preInsertedContent
|
||||
self.composeStatusAttribute.composeContent.value = preInsertedContent
|
||||
}
|
||||
|
||||
} else if case let .hashtag(text) = composeKind {
|
||||
let initialComposeContent = "#" + text
|
||||
UITextChecker.learnWord(initialComposeContent)
|
||||
let preInsertedContent = initialComposeContent + " "
|
||||
|
|
|
@ -218,6 +218,10 @@ extension HashtagTimelineViewController: UITableViewDelegate {
|
|||
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||
|
|
|
@ -114,6 +114,10 @@ extension FavoriteViewController: UITableViewDelegate {
|
|||
aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching
|
||||
|
|
|
@ -32,6 +32,7 @@ protocol StatusTableViewCellDelegate: class {
|
|||
func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, replyButtonDidPressed sender: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
||||
|
||||
|
@ -302,19 +303,21 @@ extension StatusTableViewCell: MosaicImageViewContainerDelegate {
|
|||
|
||||
// MARK: - ActionToolbarContainerDelegate
|
||||
extension StatusTableViewCell: ActionToolbarContainerDelegate {
|
||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {
|
||||
|
||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) {
|
||||
delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, replyButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) {
|
||||
delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, reblogButtonDidPressed: sender)
|
||||
}
|
||||
|
||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) {
|
||||
delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, likeButtonDidPressed: sender)
|
||||
}
|
||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, bookmarkButtonDidPressed sender: UIButton) {
|
||||
|
||||
}
|
||||
func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -88,8 +88,6 @@ extension ThreadViewController {
|
|||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// force readable layout frame update
|
||||
tableView.reloadData()
|
||||
aspectViewWillAppear(animated)
|
||||
}
|
||||
|
||||
|
@ -104,7 +102,10 @@ extension ThreadViewController {
|
|||
extension ThreadViewController {
|
||||
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
guard let rootItem = viewModel.rootItem.value,
|
||||
case let .root(statusObjectID, _) = rootItem else { return }
|
||||
let composeViewModel = ComposeViewModel(context: context, composeKind: .reply(repliedToStatusObjectID: statusObjectID))
|
||||
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -86,8 +86,8 @@ extension APIService.CoreData {
|
|||
let object = Poll.insert(into: managedObjectContext, property: Poll.Property(id: poll.id, expiresAt: poll.expiresAt, expired: poll.expired, multiple: poll.multiple, votesCount: poll.votesCount, votersCount: poll.votersCount, networkDate: networkDate), votedBy: votedBy, options: options)
|
||||
return object
|
||||
}
|
||||
let metions = entity.mentions?.compactMap { mention -> Mention in
|
||||
Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url))
|
||||
let metions = entity.mentions?.enumerated().compactMap { index, mention -> Mention in
|
||||
Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url), index: index)
|
||||
}
|
||||
let emojis = entity.emojis?.compactMap { emoji -> Emoji in
|
||||
Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category))
|
||||
|
|
|
@ -98,6 +98,7 @@ extension Mastodon.API.Statuses {
|
|||
public let mediaIDs: [String]?
|
||||
public let pollOptions: [String]?
|
||||
public let pollExpiresIn: Int?
|
||||
public let inReplyToID: Mastodon.Entity.Status.ID?
|
||||
public let sensitive: Bool?
|
||||
public let spoilerText: String?
|
||||
public let visibility: Mastodon.Entity.Status.Visibility?
|
||||
|
@ -107,6 +108,7 @@ extension Mastodon.API.Statuses {
|
|||
mediaIDs: [String]?,
|
||||
pollOptions: [String]?,
|
||||
pollExpiresIn: Int?,
|
||||
inReplyToID: Mastodon.Entity.Status.ID?,
|
||||
sensitive: Bool?,
|
||||
spoilerText: String?,
|
||||
visibility: Mastodon.Entity.Status.Visibility?
|
||||
|
@ -115,10 +117,10 @@ extension Mastodon.API.Statuses {
|
|||
self.mediaIDs = mediaIDs
|
||||
self.pollOptions = pollOptions
|
||||
self.pollExpiresIn = pollExpiresIn
|
||||
self.inReplyToID = inReplyToID
|
||||
self.sensitive = sensitive
|
||||
self.spoilerText = spoilerText
|
||||
self.visibility = visibility
|
||||
|
||||
}
|
||||
|
||||
var contentType: String? {
|
||||
|
@ -136,6 +138,7 @@ extension Mastodon.API.Statuses {
|
|||
data.append(Data.multipart(key: "poll[options][]", value: pollOption))
|
||||
}
|
||||
pollExpiresIn.flatMap { data.append(Data.multipart(key: "poll[expires_in]", value: $0)) }
|
||||
inReplyToID.flatMap { data.append(Data.multipart(key: "in_reply_to_id", value: $0)) }
|
||||
sensitive.flatMap { data.append(Data.multipart(key: "sensitive", value: $0)) }
|
||||
spoilerText.flatMap { data.append(Data.multipart(key: "spoiler_text", value: $0)) }
|
||||
visibility.flatMap { data.append(Data.multipart(key: "visibility", value: $0.rawValue)) }
|
||||
|
|
Loading…
Reference in New Issue