Skip to content

Commit

Permalink
New timeline (#276) (#280)
Browse files Browse the repository at this point in the history
* Fixes #276 - Rebuilt room timeline:
    - Removed the need for the ListCollectionViewAdapter
    - Rewrote the TimelineItemList without using introspection
    - Added ReversedScrollView for laying out items at the bottom/trailing
    - Rewrote TimelineProvider diffing through CollectionDifference (similar to the RoomSummaryProvider)
    - Added back `scrollDismissesKeyboard`  behavior
    - Various other tweaks and fixes
- Fixed various warnings:
    - removed async AttributedStringBuilder as AttributedString is non-sendable, made the RoomTimelineItemFactory synchronous
    - removed unused virtual timeline items
    - removed unused isOutgoing property from the FormattedBodyText
* Make TimelineItemContextMenuActions indentifiable and specify contextMenu identifiers
* Bump the matrix-rust-components-swift to v1.0.16-alpha
* Add changes file and changelog contribution guide
* Fix attributed string builder unit tests
  • Loading branch information
stefanceriu authored Nov 2, 2022
1 parent b270b8a commit fabb0bc
Show file tree
Hide file tree
Showing 31 changed files with 515 additions and 583 deletions.
52 changes: 52 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,58 @@ Please see our [pull request guide](https://github.com/vector-im/element-android

New screen flows are currently using MVVM-Coordinator pattern. Please refer to the screen template under [Tools/Scripts/createScreen.sh](Tools/Scripts/createScreen.sh) to create a new screen or a new screen flow.

## Changelog

All changes, even minor ones, need a corresponding changelog / newsfragment
entry. These are managed by [Towncrier](https://github.com/twisted/towncrier).

To create a changelog entry, make a new file in the `changelog.d` directory
named in the format of `ElementXiOSIssueNumber.type`. The type can be one of the
following:

- `feature` for a new feature
- `change` for updates to an existing feature
- `bugfix` for bug fix
- `api` for an api break
- `i18n` for translations
- `build` for changes related to build, tools, CI/CD
- `doc` for updates to the documentation
- `wip` for anything that isn't ready to ship and will be enabled at a later date
- `misc` for other changes

This file will become part of our [changelog](CHANGES.md) at the next
release, so the content of the file should be a short description of your
change in the same style as the rest of the changelog. The file must only
contain one line. It can contain Markdown formatting. It should start with the
area of the change (screen, module, ...) and end with a full stop (.) or an
exclamation mark (!) for consistency.

Adding credits to the changelog is encouraged, we value your
contributions and would like to have you shouted out in the release notes!

For example, a fix for an issue #1234 would have its changelog entry in
`changelog.d/1234.bugfix`, and contain content like:

> Voice Messages: Fix a crash when sending a voice message. Contributed by
> Jane Matrix.
If there are multiple pull requests involved in a single bugfix/feature/etc,
then the content for each `changelog.d` file should be the same. Towncrier will
merge the matching files together into a single changelog entry when we come to
release.

There are exceptions on the `ElementXiOSIssueNumber.type` entry format. Even if
it is not encouraged, you can use:

- `pr-[PRNumber].type` for a PR with no related issue
- `x-nolink-[AnyNumber].type` for a PR with a change entry that will not have a link automatically appended. It must be used for internal project update only. `AnyNumber` should be a value that does not clash with existing files.

To preview the changelog for pending changelog entries, use:

```bash
$ towncrier build --draft --version 1.2.3
```

## Coding style

For Swift coding style we use [SwiftLint](https://github.com/realm/SwiftLint) to check some conventions at compile time (rules are located in the `.swiftlint.yml` file).
Expand Down
98 changes: 37 additions & 61 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-rust-components-swift",
"state" : {
"revision" : "e3ae8941c864ba18e7e5c51d25a6ce2c5c7a65a0",
"version" : "1.0.15-alpha"
"revision" : "af6683ca5ddd9da7f582de4258e4d6ebb8a8caff",
"version" : "1.0.16-alpha"
}
},
{
Expand Down
16 changes: 2 additions & 14 deletions ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
private let temporaryBlockquoteMarkingColor = UIColor.magenta
private let temporaryCodeBlockMarkingColor = UIColor.cyan
private let linkColor = UIColor.blue

func fromPlain(_ string: String?) async -> AttributedString? {
await Task.dispatch(on: .global()) {
fromPlain(string)
}
}


func fromPlain(_ string: String?) -> AttributedString? {
guard let string else {
return nil
Expand All @@ -39,13 +33,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {

return try? AttributedString(mutableAttributedString, including: \.elementX)
}

func fromHTML(_ htmlString: String?) async -> AttributedString? {
await Task.dispatch(on: .global()) {
fromHTML(htmlString)
}
}


// Do not use the default HTML renderer of NSAttributedString because this method
// runs on the UI thread which we want to avoid because renderHTMLString is called
// most of the time from a background thread.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ struct AttributedStringBuilderComponent: Hashable {

protocol AttributedStringBuilderProtocol {
func fromPlain(_ string: String?) -> AttributedString?
func fromPlain(_ string: String?) async -> AttributedString?

func fromHTML(_ htmlString: String?) -> AttributedString?
func fromHTML(_ htmlString: String?) async -> AttributedString?

func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]?
}
82 changes: 82 additions & 0 deletions ElementX/Sources/Other/ReversedScrollView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import SwiftUI

/// A SwiftUI scroll view that lays out its content starting at the bottom or trailing
/// https://www.thirdrocktechkno.com/blog/implementing-reversed-scrolling-behaviour-in-swiftui/
struct ReversedScrollView<Content: View>: View {
private let axis: Axis.Set
private let leadingSpace: CGFloat
private let content: Content

init(_ axis: Axis.Set = .horizontal, leadingSpace: CGFloat = 0, @ViewBuilder builder: () -> Content) {
self.axis = axis
self.leadingSpace = leadingSpace
content = builder()
}

var body: some View {
GeometryReader { proxy in
ScrollView(axis, showsIndicators: false) {
Stack(axis) {
Spacer(minLength: leadingSpace)
content
}
.frame(
minWidth: minWidth(in: proxy, for: axis),
minHeight: minHeight(in: proxy, for: axis)
)
}
}
}

private func minWidth(in proxy: GeometryProxy, for axis: Axis.Set) -> CGFloat? {
axis.contains(.horizontal) ? proxy.size.width : nil
}

private func minHeight(in proxy: GeometryProxy, for axis: Axis.Set) -> CGFloat? {
axis.contains(.vertical) ? proxy.size.height : nil
}
}

private struct Stack<Content: View>: View {
var axis: Axis.Set
var content: Content

init(_ axis: Axis.Set = .vertical, @ViewBuilder builder: () -> Content) {
self.axis = axis

content = builder()
}

var body: some View {
switch axis {
case .horizontal:
HStack {
content
}
case .vertical:
VStack {
content
}
default:
VStack {
content
}
}
}
}
4 changes: 3 additions & 1 deletion ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import UIKit

enum RoomScreenViewModelAction { }

enum TimelineItemContextMenuAction: Hashable {
enum TimelineItemContextMenuAction: Identifiable, Hashable {
case copy
case quote
case copyPermalink
case redact
case reply

var id: Self { self }
}

enum RoomScreenComposerMode: Equatable {
Expand Down
10 changes: 8 additions & 2 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}

self.state.items[viewIndex] = timelineViewFactory.buildTimelineViewFor(timelineItem: timelineItem)
case .startedBackPaginating:
self.state.isBackPaginating = true
case .finishedBackPaginating:
self.state.isBackPaginating = false
}
}
.store(in: &cancellables)
Expand All @@ -81,11 +85,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
override func process(viewAction: RoomScreenViewAction) async {
switch viewAction {
case .loadPreviousPage:
state.isBackPaginating = true
guard !state.isBackPaginating else {
return
}

switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) {
default:
state.isBackPaginating = false
#warning("Treat errors")
}
case .itemAppeared(let id):
await timelineController.processItemAppearance(id)
Expand Down
Loading

0 comments on commit fabb0bc

Please sign in to comment.