Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CIS-42] Markdown support for messages #2067

Merged
merged 10 commits into from
Jun 16, 2022
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ excluded:
- Dependencies
- Sources/StreamChatUI/Generated
- Sources/StreamChatUI/StreamSwiftyGif
- Sources/StreamChatUI/StreamSwiftyMarkdown
hugobernalstream marked this conversation as resolved.
Show resolved Hide resolved
- Sources/StreamChatUI/StreamNuke
- Sources/StreamChat/StreamStarscream
- vendor
Expand Down
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,24 @@ update_dependencies:
make update_starscream version=4.0.4
echo "👉 Updating SwiftyGif"
make update_swiftygif version=5.4.2
echo "👉 Updating SwiftyMarkdown"
make update_swiftymarkdown version=1.2.4

update_nuke: check_version_parameter
./Scripts/updateDependency.sh $(version) Dependencies/Nuke Sources/StreamChatUI/StreamNuke Sources
./Scripts/removePublicDeclaracions.sh Sources/StreamChatUI/StreamNuke
./Scripts/removePublicDeclarations.sh Sources/StreamChatUI/StreamNuke

update_starscream: check_version_parameter
./Scripts/updateDependency.sh $(version) Dependencies/Starscream Sources/StreamChat/StreamStarscream Sources
./Scripts/removePublicDeclaracions.sh Sources/StreamChat/StreamStarscream
./Scripts/removePublicDeclarations.sh Sources/StreamChat/StreamStarscream

update_swiftygif: check_version_parameter
./Scripts/updateDependency.sh $(version) Dependencies/SwiftyGif Sources/StreamChatUI/StreamSwiftyGif SwiftyGif
./Scripts/removePublicDeclaracions.sh Sources/StreamChatUI/StreamSwiftyGif
./Scripts/removePublicDeclarations.sh Sources/StreamChatUI/StreamSwiftyGif

update_swiftymarkdown: check_version_parameter
./Scripts/updateDependency.sh $(version) Dependencies/SwiftyMarkdown Sources/StreamChatUI/StreamSwiftyMarkdown Sources
./Scripts/removePublicDeclarations.sh Sources/StreamChatUI/StreamSwiftyMarkdown

check_version_parameter:
@if [ "$(version)" = "" ]; then\
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# Usage: ./removePublicDeclaracions.sh Sources/StreamNuke
# Usage: ./removePublicDeclarations.sh Sources/StreamNuke
#
# This script would iterate over the files on a particular directory, and perform basic replacement operations.
# It heavily relies on 'sed':
Expand All @@ -14,6 +14,7 @@ directory=$1
files=`find $directory -name "*.swift"`
for f in $files
do
`sed -i '' -e 's/public internal(set) //g' -e 's/open //g' $f`
`sed -i '' -e 's/public //g' -e 's/open //g' $f`

# Nuke
Expand All @@ -36,4 +37,4 @@ do
`sed -i '' -e 's/ConnectionEvent/StarscreamConnectionEvent/g' $f`
`sed -i '' -e 's/: class {/: AnyObject {/g' $f`
fi
done
done
2 changes: 1 addition & 1 deletion Scripts/run-linter.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set -euo pipefail
echo -e "👉 Running SwiftFormat Linting"

echo -e "👉 Linting Sources..."
mint run swiftformat --lint --config .swiftformat Sources --exclude **/Generated,Sources/StreamChat/StreamStarscream,Sources/StreamChatUI/StreamNuke,Sources/StreamChatUI/StreamSwiftyGif
mint run swiftformat --lint --config .swiftformat Sources --exclude **/Generated,Sources/StreamChat/StreamStarscream,Sources/StreamChatUI/StreamNuke,Sources/StreamChatUI/StreamSwiftyGif,Sources/StreamChatUI/StreamSwiftyMarkdown
echo -e "👉 Linting Tests..."
mint run swiftformat --lint --config .swiftformat Tests
echo -e "👉 Linting Sample..."
Expand Down
2 changes: 2 additions & 0 deletions Scripts/updateDependency.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ elif [[ $dependency_directory == *"SwiftyGif"* ]]; then
dependency_url="git@github.com:kirualex/SwiftyGif.git"
elif [[ $dependency_directory == *"Starscream"* ]]; then
dependency_url="git@github.com:daltoniam/Starscream.git"
elif [[ $dependency_directory == *"SwiftyMarkdown"* ]]; then
dependency_url="git@github.com:SimonFairbairn/SwiftyMarkdown.git"
else
echo "→ Unknown dependency at $dependency_directory"
exit 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ open class ChatMessageContentView: _View, ThemeProvider {
/// When this value is set the subviews are instantiated and laid out just once based on
/// the received options.
public var layoutOptions: ChatMessageLayoutOptions?

/// The formatter used for text Mardown
hugobernalstream marked this conversation as resolved.
Show resolved Hide resolved
public var markdownFormatter: MarkdownFormatter = DefaultMarkdownFormatter()
evsaev marked this conversation as resolved.
Show resolved Hide resolved

/// A boolean value that determines whether Markdown is active for messages to be formatted.
open var markdownFormatterEnabled: Bool {
components.markdownFormatterEnabled
}

// MARK: Content && Actions

Expand Down Expand Up @@ -493,6 +501,11 @@ open class ChatMessageContentView: _View, ThemeProvider {
textView?.font = textFont
textView?.text = content?.textContent

if markdownFormatterEnabled, markdownFormatter.containsMarkdown(text: content?.textContent ?? "") {
let markdownText = markdownFormatter.format(from: content?.textContent ?? "")
textView?.attributedText = markdownText
}
evsaev marked this conversation as resolved.
Show resolved Hide resolved

// Avatar
let placeholder = appearance.images.userAvatarPlaceholder1
if let imageURL = content?.author.imageURL, let imageView = authorAvatarView?.imageView {
Expand Down
6 changes: 6 additions & 0 deletions Sources/StreamChatUI/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ public struct Components {
/// Note: This is currently an experimental feature that we are actively
/// working on and testing to make sure it is stable.
public var _messageListDiffingEnabled = false

/// A boolean value that determines whether Markdown is active for messages to be formatted.
public var markdownFormatterEnabled = true
bielikb marked this conversation as resolved.
Show resolved Hide resolved

/// The view controller used to perform message actions.
public var messageActionsVC: ChatMessageActionsVC.Type = ChatMessageActionsVC.self
Expand Down Expand Up @@ -327,6 +330,9 @@ public struct Components {
/// A view that displays the video attachment preview in composer.
public var videoAttachmentComposerPreview: VideoAttachmentComposerPreview
.Type = VideoAttachmentComposerPreview.self

/// A formatter used for text Markdown
public var markdownFormatter: MarkdownFormatter.Type = DefaultMarkdownFormatter.self
bielikb marked this conversation as resolved.
Show resolved Hide resolved

// MARK: - Composer suggestion components

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// Copyright © 2022 Stream.io Inc. All rights reserved.
hugobernalstream marked this conversation as resolved.
Show resolved Hide resolved
//

import Foundation

enum SpaceAllowed {
case no
case bothSides
case oneSide
case leadingSide
case trailingSide
}

enum Cancel {
case none
case allRemaining
case currentSet
}

enum CharacterRuleTagType {
case open
case close
case metadataOpen
case metadataClose
case repeating
}

struct CharacterRuleTag {
let tag: String
let type: CharacterRuleTagType

init(tag: String, type: CharacterRuleTagType) {
self.tag = tag
self.type = type
}
}

struct CharacterRule: CustomStringConvertible {
let primaryTag: CharacterRuleTag
let tags: [CharacterRuleTag]
let escapeCharacters: [Character]
let styles: [Int: CharacterStyling]
let minTags: Int
let maxTags: Int
var metadataLookup: Bool = false
var isRepeatingTag: Bool {
primaryTag.type == .repeating
}

var definesBoundary = false
var shouldCancelRemainingRules = false
var balancedTags = false

var description: String {
"Character Rule with Open tag: \(primaryTag.tag) and current styles : \(styles) "
}

func tag(for type: CharacterRuleTagType) -> CharacterRuleTag? {
tags.filter { $0.type == type }.first ?? nil
}

init(
primaryTag: CharacterRuleTag,
otherTags: [CharacterRuleTag],
escapeCharacters: [Character] = ["\\"],
styles: [Int: CharacterStyling] = [:],
minTags: Int = 1,
maxTags: Int = 1,
metadataLookup: Bool = false,
definesBoundary: Bool = false,
shouldCancelRemainingRules: Bool = false,
balancedTags: Bool = false
) {
self.primaryTag = primaryTag
tags = otherTags
self.escapeCharacters = escapeCharacters
self.styles = styles
self.metadataLookup = metadataLookup
self.definesBoundary = definesBoundary
self.shouldCancelRemainingRules = shouldCancelRemainingRules
self.minTags = maxTags < minTags ? maxTags : minTags
self.maxTags = minTags > maxTags ? minTags : maxTags
self.balancedTags = balancedTags
}
}

enum ElementType {
case tag
case escape
case string
case space
case newline
case metadata
}

struct Element {
let character: Character
var type: ElementType
var boundaryCount: Int = 0
var isComplete: Bool = false
var styles: [CharacterStyling] = []
var metadata: [String] = []
}

extension CharacterSet {
func containsUnicodeScalars(of character: Character) -> Bool {
character.unicodeScalars.allSatisfy(contains(_:))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Copyright © 2022 Stream.io Inc. All rights reserved.
//

import Foundation
import os.log

class PerformanceLog {
var timer: TimeInterval = 0
let enablePerfomanceLog: Bool
let log: OSLog
let identifier: String

init(with environmentVariableName: String, identifier: String, log: OSLog) {
self.log = log
enablePerfomanceLog = (ProcessInfo.processInfo.environment[environmentVariableName] != nil)
self.identifier = identifier
}

func start() {
guard enablePerfomanceLog else { return }
timer = Date().timeIntervalSinceReferenceDate
os_log("--- TIMER %{public}@ began", log: log, type: .info, identifier)
}

func tag(with string: String) {
guard enablePerfomanceLog else { return }
if timer == 0 {
start()
}
os_log("TIMER %{public}@: %f %@", log: log, type: .info, identifier, Date().timeIntervalSinceReferenceDate - timer, string)
}

func end() {
guard enablePerfomanceLog else { return }
timer = Date().timeIntervalSinceReferenceDate
os_log(
"--- TIMER %{public}@ finished. Total time: %f",
log: log,
type: .info,
identifier,
Date().timeIntervalSinceReferenceDate - timer
)
timer = 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// Copyright © 2022 Stream.io Inc. All rights reserved.
//

import Foundation

/// Some helper functions based on this:
/// https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift/32306142#32306142
extension StringProtocol {
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.lowerBound
}

func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.upperBound
}

func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
var indices: [Index] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...]
.range(of: string, options: options) {
indices.append(range.lowerBound)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return indices
}

func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<String.Index>] {
var result: [Range<Index>] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...]
.range(of: string, options: options) {
result.append(range)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return result
}
}
Loading