feat: implement reply status entry and update query of API

This commit is contained in:
CMK 2021-04-14 15:59:29 +08:00
parent 0eff43e1d1
commit d5c9473528
16 changed files with 126 additions and 19 deletions

View File

@ -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"/>
@ -217,4 +218,4 @@
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
</elements>
</model>
</model>

View File

@ -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

View File

@ -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):

View File

@ -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)")

View File

@ -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)
}

View File

@ -277,7 +277,6 @@ extension StatusProviderFacade {
}
extension StatusProviderFacade {
static func responseToStatusReblogAction(provider: StatusProvider) {
_responseToStatusReblogAction(
@ -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

View File

@ -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

View File

@ -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)))")

View File

@ -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

View File

@ -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 + " "

View File

@ -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

View File

@ -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

View File

@ -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) {
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) {
}
}

View File

@ -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))
}
}

View File

@ -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))

View File

@ -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)) }