Skip to content

Commit

Permalink
[CIS-42] Markdown support for messages (#2067)
Browse files Browse the repository at this point in the history
* Add Markdown support using SwiftyMarkdown library
  • Loading branch information
hugobernalstream committed Jun 16, 2022
1 parent ace2a6d commit e635ab7
Show file tree
Hide file tree
Showing 26 changed files with 2,718 additions and 20 deletions.
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
- Sources/StreamChatUI/StreamNuke
- Sources/StreamChat/StreamStarscream
- vendor
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### ✅ Added
- Show typing users within a thread [#2080](https://github.com/GetStream/stream-chat-swift/issues/2080)

## StreamChatUI
### ✅ Added
- Add support for Markdown syntax [#2067](https://github.com/GetStream/stream-chat-swift/pull/2067)
### 🐞 Fixed
- Fix Logger persisting config after usage, preventing changing parameters (such as LogLevel) [#2081](https://github.com/GetStream/stream-chat-swift/issues/2081)

# [4.16.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.16.0)
_June 10, 2022_
## StreamChat
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 @@ -26,5 +26,11 @@ public extension Appearance {

/// A formatter that generates a name for the given channel.
public var channelName: ChannelNameFormatter = DefaultChannelNameFormatter()

/// A formatter used for text Markdown
public var markdownFormatter: MarkdownFormatter = DefaultMarkdownFormatter()

/// A boolean value that determines whether Markdown is active for messages to be formatted.
public var isMarkdownEnabled = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ 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 Markdown
public var markdownFormatter: MarkdownFormatter {
appearance.formatters.markdownFormatter
}

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

// MARK: Content && Actions

Expand Down Expand Up @@ -477,22 +487,27 @@ open class ChatMessageContentView: _View, ThemeProvider {
}

// Text
var textColor = appearance.colorPalette.text
var textFont = appearance.fonts.body

if content?.isDeleted == true {
textColor = appearance.colorPalette.textLowEmphasis
} else if content?.shouldRenderAsJumbomoji == true {
textFont = appearance.fonts.emoji
} else if content?.type == .system || content?.type == .error {
textFont = appearance.fonts.caption1.bold
textColor = appearance.colorPalette.textLowEmphasis
if isMarkdownEnabled, markdownFormatter.containsMarkdown(content?.textContent ?? "") {
let markdownText = markdownFormatter.format(content?.textContent ?? "")
textView?.attributedText = markdownText
} else {
var textColor = appearance.colorPalette.text
var textFont = appearance.fonts.body

if content?.isDeleted == true {
textColor = appearance.colorPalette.textLowEmphasis
} else if content?.shouldRenderAsJumbomoji == true {
textFont = appearance.fonts.emoji
} else if content?.type == .system || content?.type == .error {
textFont = appearance.fonts.caption1.bold
textColor = appearance.colorPalette.textLowEmphasis
}

textView?.textColor = textColor
textView?.font = textFont
textView?.text = content?.textContent
}

textView?.textColor = textColor
textView?.font = textFont
textView?.text = content?.textContent

// Avatar
let placeholder = appearance.images.userAvatarPlaceholder1
if let imageURL = content?.author.imageURL, let imageView = authorAvatarView?.imageView {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//
// Copyright © 2022 Stream.io Inc. All rights reserved.
//

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

0 comments on commit e635ab7

Please sign in to comment.