Add snapshot pill for incoming mentions

This commit is contained in:
aringenbach
2022-04-14 13:30:08 +02:00
parent f62b876f54
commit b11a653f1c
3 changed files with 159 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
//
// 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 Foundation
// TODO: replace this with directly creating NSAttributedString with both link and attachment (removes weird interaction between two objects here)
@objcMembers class PillTextAttachment: NSTextAttachment {
convenience init(withSession session: MXSession, url: NSURL, andRoomMember roomMember: MXRoomMember) {
self.init()
let image = PillSnapshoter.snapshot(withSession: session, andRoomMember: roomMember)
self.image = image
// FIXME: handle vertical offset better
self.bounds = CGRect(x: 0.0, y: -5.0, width: image.size.width * 0.3, height: image.size.height * 0.3)
}
}
@objcMembers class PillSnapshoter: NSObject {
static func mentionPill(withSession session: MXSession, url: NSURL, andRoomMember roomMember: MXRoomMember) -> NSAttributedString {
let attachment = PillTextAttachment(withSession: session, url: url, andRoomMember: roomMember)
let string = NSAttributedString(attachment: attachment)
let mutable = NSMutableAttributedString(attributedString: string)
mutable.addAttribute(.link, value: url, range: .init(location: 0, length: mutable.length))
return mutable
}
static func snapshot(withSession session: MXSession, andRoomMember roomMember: MXRoomMember) -> UIImage {
let view = snapshotView(withSession: session, andRoomMember: roomMember)
let rect: CGRect = view.frame
UIGraphicsBeginImageContext(rect.size)
let context: CGContext = UIGraphicsGetCurrentContext()!
view.layer.render(in: context)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}
// TODO: Improve how image scale is handled
// TODO: Implement a solution with image cache to increase performance
private static func snapshotView(withSession session: MXSession, andRoomMember roomMember: MXRoomMember) -> UIView {
let label = UILabel(frame: .zero)
label.text = roomMember.displayname
label.font = ThemeService.shared().theme.fonts.body.withSize(ThemeService.shared().theme.fonts.body.pointSize * 2.0)
label.textColor = ThemeService.shared().theme.textPrimaryColor
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
height: CGFloat.greatestFiniteMagnitude))
label.frame = CGRect(x: 52 + 16, y: 0, width: labelSize.width, height: 60)
let view = UIView(frame: CGRect(x: 0, y: 0, width: labelSize.width + 16 + 52 + 16, height: 60))
// FIXME: handle avatar not being in cache at snapshot time
let imageView = MXKImageView(frame: CGRect(x: 8, y: 4, width: 60, height: 60))
imageView.setImageURI(roomMember.avatarUrl,
withType: nil,
andImageOrientation: .up,
toFitViewSize: imageView.frame.size,
with: MXThumbnailingMethodCrop,
previewImage: Asset.Images.userIcon.image,
mediaManager: session.mediaManager)
imageView.clipsToBounds = true
imageView.frame = CGRect(x: 8, y: 4, width: 52, height: 52)
imageView.layer.cornerRadius = 26.0
view.addSubview(imageView)
view.addSubview(label)
view.backgroundColor = ThemeService.shared().theme.secondaryCircleButtonBackgroundColor
view.layer.cornerRadius = 30
return view
}
}
+66
View File
@@ -0,0 +1,66 @@
//
// 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 Foundation
@objcMembers
class StringPillsUtils: NSObject {
// MARK: - Private Constants
private enum Constants {
// TODO: replace this with a solution handling any kind of custom permalinks.
static let matrixToURL: String = "https://matrix.to/#/"
}
// MARK: - Internal Methods
static func insertPills(in attributedString: NSAttributedString,
withSession session: MXSession,
andRoomState roomState: MXRoomState) -> NSAttributedString {
// TODO: Improve algorithm & cleanup this method
let newAttr = NSMutableAttributedString(attributedString: attributedString)
var lastIndex: Int = 0
while lastIndex < newAttr.length {
var url: NSURL?
let linkRange = newAttr.rangeOfLink(at: UInt(lastIndex), url: &url)
guard let url = url,
// FIXME: remove this check if only encrypted message replacer sets non-URL objects in NSLink attributes
url.isKind(of: NSURL.self),
let absoluteString = url.absoluteString,
absoluteString.starts(with: Constants.matrixToURL)
else {
lastIndex += 1
continue
}
let userId = String(absoluteString.dropFirst(Constants.matrixToURL.count))
if linkRange.length > 0 {
guard let roomMember = roomState.members.member(withUserId: userId) else {
lastIndex += linkRange.length
continue
}
let attachmentString = PillSnapshoter.mentionPill(withSession: session, url: url, andRoomMember: roomMember)
newAttr.replaceCharacters(in: linkRange, with: attachmentString)
lastIndex += attachmentString.length
} else {
lastIndex += 1
}
}
return newAttr
}
}