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

Support for dynamic username coloring #152

Merged
merged 2 commits into from
Sep 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Moc.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@
43F610FB28AFB8AE0098C3BD /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F610FA28AFB8AE0098C3BD /* VisualEffectView.swift */; };
43F610FE28B01E830098C3BD /* MessageBubbleShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F610FD28B01E830098C3BD /* MessageBubbleShape.swift */; };
43F7C504287DCFB40083E9E9 /* ScrollOffsetPreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F7C503287DCFB40083E9E9 /* ScrollOffsetPreferenceKey.swift */; };
DF12907A28D6276700F5F9D7 /* Color+ContrastRatio.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF12907928D6276700F5F9D7 /* Color+ContrastRatio.swift */; };
DF12907C28D6276F00F5F9D7 /* CGFloat+Convertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF12907B28D6276F00F5F9D7 /* CGFloat+Convertions.swift */; };
DF12907E28D6277600F5F9D7 /* Color+DynamicUserID.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF12907D28D6277600F5F9D7 /* Color+DynamicUserID.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -303,6 +306,9 @@
43F610FD28B01E830098C3BD /* MessageBubbleShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubbleShape.swift; sourceTree = "<group>"; };
43F7C503287DCFB40083E9E9 /* ScrollOffsetPreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollOffsetPreferenceKey.swift; sourceTree = "<group>"; };
43F871B7283E21C700B6C717 /* Logs */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Logs; sourceTree = "<group>"; };
DF12907928D6276700F5F9D7 /* Color+ContrastRatio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Color+ContrastRatio.swift"; path = "../../../../Downloads/Color+ContrastRatio.swift"; sourceTree = "<group>"; };
DF12907B28D6276F00F5F9D7 /* CGFloat+Convertions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "CGFloat+Convertions.swift"; path = "../../../../Downloads/CGFloat+Convertions.swift"; sourceTree = "<group>"; };
DF12907D28D6277600F5F9D7 /* Color+DynamicUserID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Color+DynamicUserID.swift"; path = "../../../../Downloads/Color+DynamicUserID.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -361,6 +367,9 @@
435C9C0D28A1915F00E5D682 /* Notification.Name+Constants.swift */,
43505ECD28BE16FA00E8C5CD /* Color+Additional.swift */,
43C227CA28CA344E00AE237C /* ConnectionState+Title.swift */,
DF12907B28D6276F00F5F9D7 /* CGFloat+Convertions.swift */,
DF12907D28D6277600F5F9D7 /* Color+DynamicUserID.swift */,
DF12907928D6276700F5F9D7 /* Color+ContrastRatio.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -945,6 +954,7 @@
43C0F49A285A58BD009F2419 /* View+SidebarSize.swift in Sources */,
43807D9427E213350056A3D3 /* MessageView.swift in Sources */,
43807D8E27E213350056A3D3 /* DevicesPrefView.swift in Sources */,
DF12907C28D6276F00F5F9D7 /* CGFloat+Convertions.swift in Sources */,
43B1029B28831A8D009FCF53 /* Image+Data.swift in Sources */,
439BA4282863C3CC00339375 /* LoginView+Welcome.swift in Sources */,
43C227D128CB097A00AE237C /* View+Faded.swift in Sources */,
Expand Down Expand Up @@ -972,6 +982,7 @@
433EDD942860E7CB009D03D5 /* QuickLookPreviewWrapper.swift in Sources */,
43B5D7BE28996ED000180E65 /* UpdateManager.swift in Sources */,
43807D8227E213350056A3D3 /* ChatType.swift in Sources */,
DF12907A28D6276700F5F9D7 /* Color+ContrastRatio.swift in Sources */,
43807D9C27E213350056A3D3 /* RootView.swift in Sources */,
43051C9B28779AE2003C5CE2 /* ChatViewModel+Updates.swift in Sources */,
438B2283285E3098000D65C8 /* BackportedGradient.swift in Sources */,
Expand All @@ -990,6 +1001,7 @@
439BA42A2863C43000339375 /* LoginView+PhoneNumber.swift in Sources */,
4367072B286229B000587A63 /* MessageView+Photo.swift in Sources */,
439BA42C2863C4B200339375 /* LoginView+Code.swift in Sources */,
DF12907E28D6277600F5F9D7 /* Color+DynamicUserID.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
47 changes: 47 additions & 0 deletions Shared/Utils/CGFloat+Convertions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// CGFloat+Convertions.swift

import Foundation

extension CGFloat {
/// clamp the supplied value between a min and max
/// - Parameter min: The min value
/// - Parameter max: The max value
func clamp(min: CGFloat, max: CGFloat) -> CGFloat {
if self < min {
return min
} else if self > max {
return max
} else {
return self
}
}

/// If colour value is less than 1, add 1 to it. If temp colour value is greater than 1, substract 1 from it
func convertToColourChannel() -> CGFloat {
let min: CGFloat = 0
let max: CGFloat = 1
let modifier: CGFloat = 1
if self < min {
return self + modifier
} else if self > max {
return self - max
} else {
return self
}
}

/// Formula to convert the calculated colour from colour multipliers
/// - Parameter temp1: Temp variable one (calculated from luminosity)
/// - Parameter temp2: Temp variable two (calcualted from temp1 and luminosity)
func convertToRGB(temp1: CGFloat, temp2: CGFloat) -> CGFloat {
if 6 * self < 1 {
return temp2 + (temp1 - temp2) * 6 * self
} else if 2 * self < 1 {
return temp1
} else if 3 * self < 2 {
return temp2 + (temp1 - temp2) * (0.666 - self) * 6
} else {
return temp2
}
}
}
39 changes: 39 additions & 0 deletions Shared/Utils/Color+ContrastRatio.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Color+ContrastRatio.swift

import SwiftUI

// Source:
// https://stackoverflow.com/questions/
// 42355778/how-to-compute-color-contrast-ratio-between-two-uicolor-instances
extension Color {
// swiftlint:disable large_tuple
var components: (red: CGFloat, green: CGFloat, blue: CGFloat, opacity: CGFloat) {
let components = self.cgColor?.components ?? [0, 0, 0, 0]
return (components[0], components[1], components[2], components[3])
}

func contrastRatio() -> Double {
let luminance1 = self.luminance()
let luminance2 = Color("MessageFromRecepientColor").luminance()

let luminanceDarker = min(luminance1, luminance2)
let luminanceLighter = max(luminance1, luminance2)

let contrast = (luminanceLighter + 0.05) / (luminanceDarker + 0.05)

return contrast
}

func luminance() -> Double {
func adjust(colorComponent: CGFloat) -> CGFloat {
return (colorComponent < 0.04045) ? (colorComponent / 12.92) : pow((colorComponent + 0.055) / 1.055, 2.4)
}

let components = self.components

return 0.2126 * adjust(
colorComponent: components.red) + 0.7152 * adjust(
colorComponent: components.green) + 0.0722 * adjust(
colorComponent: components.blue)
}
}
135 changes: 135 additions & 0 deletions Shared/Utils/Color+DynamicUserID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Color+DynamicUserID.swift

import SwiftUI
import Backend

extension Color {
init?(from userId: Int64) async throws {
// Source:
// https://www.hackingwithswift.com/example-code/media/
// how-to-read-the-average-color-of-a-uiimage-using-ciareaaverage
let user = try await TdApi.shared.getUser(userId: userId)

guard let profilePhoto = user.profilePhoto else { return nil }
guard let photo = profilePhoto.minithumbnail else { return nil }
guard let inputImage = CIImage(data: photo.data) else { return nil }

let extentVector = CIVector(
x: inputImage.extent.origin.x,
y: inputImage.extent.origin.y,
z: inputImage.extent.size.width,
w: inputImage.extent.size.height
)

guard let filter = CIFilter(
name: "CIAreaAverage",
parameters: [
kCIInputImageKey: inputImage,
kCIInputExtentKey: extentVector
]) else { return nil }
guard let outputImage = filter.outputImage else { return nil }

var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: kCFNull as Any])

context.render(
outputImage,
toBitmap: &bitmap,
rowBytes: 4,
bounds: CGRect(x: 0, y: 0, width: 1, height: 1),
format: .RGBA8,
colorSpace: nil
)

self.init(
red: CGFloat(bitmap[0]) / 255,
green: CGFloat(bitmap[1]) / 255,
blue: CGFloat(bitmap[2]) / 255,
opacity: 1
)
}

// Source:
// https://medium.com/trinity-mirror-digital/adjusting-uicolor-luminosity-in-swift-4168e3c4cdf1
// swiftlint:disable function_body_length cyclomatic_complexity
func withLuminosity(_ colorSchemeContrast: ColorSchemeContrast = .standard) -> Color {
// TODO: calculate newLuminosity
// depending on the foreground color of a message bubble
// Color.contrastRatio() may help
let newLuminosity: CGFloat
switch colorSchemeContrast {
case .standard:
newLuminosity = 0.6
case .increased:
newLuminosity = 0.8
@unknown default:
newLuminosity = 0.6
}

let nsColor = NSColor(self)
guard let coreColour = CIColor(color: nsColor) else { return self }
var red = coreColour.red
var green = coreColour.green
var blue = coreColour.blue
let alpha = coreColour.alpha

red = red.clamp(min: 0, max: 1)
green = green.clamp(min: 0, max: 1)
blue = blue.clamp(min: 0, max: 1)

guard let minRGB = [red, green, blue].min(),
let maxRGB = [red, green, blue].max() else { return self }

var luminosity = (minRGB + maxRGB) / 2

var saturation: CGFloat = 0

if luminosity <= 0.5 {
saturation = (maxRGB - minRGB)/(maxRGB + minRGB)
} else if luminosity > 0.5 {
saturation = (maxRGB - minRGB)/(2.0 - maxRGB - minRGB)
}

var hue: CGFloat = 0
if red == maxRGB {
hue = (green - blue) / (maxRGB - minRGB)
} else if green == maxRGB {
hue = 2.0 + ((blue - red) / (maxRGB - minRGB))
} else if blue == maxRGB {
hue = 4.0 + ((red - green) / (maxRGB - minRGB))
}

if hue < 0 {
hue += 360
} else {
hue *= 60
}

luminosity = newLuminosity

if saturation == 0 {
return Color(red: 1.0 * luminosity, green: 1.0 * luminosity, blue: 1.0 * luminosity, opacity: alpha)
}

var temporaryVariableOne: CGFloat = 0
if luminosity < 0.5 {
temporaryVariableOne = luminosity * (1 + saturation)
} else {
temporaryVariableOne = luminosity + saturation - luminosity * saturation
}

let temporaryVariableTwo = 2 * luminosity - temporaryVariableOne

let convertedHue = hue / 360

let tempRed = (convertedHue + 0.333).convertToColourChannel()
let tempGreen = convertedHue.convertToColourChannel()
let tempBlue = (convertedHue - 0.333).convertToColourChannel()

let newRed = tempRed.convertToRGB(temp1: temporaryVariableOne, temp2: temporaryVariableTwo)
let newGreen = tempGreen.convertToRGB(temp1: temporaryVariableOne, temp2: temporaryVariableTwo)
let newBlue = tempBlue.convertToRGB(temp1: temporaryVariableOne, temp2: temporaryVariableTwo)

return Color(red: newRed, green: newGreen, blue: newBlue, opacity: alpha)
}
}
87 changes: 49 additions & 38 deletions Shared/Views/Chat/Message/MessageView+Reply.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,54 +16,65 @@ extension MessageView {
.fixedSize(horizontal: false, vertical: true)
.lineLimit(1)
}

@ViewBuilder
func replyLabel(reply: ReplyMessage) -> some View {
HStack {
Capsule()
.if(mainMessage.isOutgoing) {
$0.fill(.white)
} else: {
$0.fill(self.replyUsernameColor?.withLuminosity(colorSchemeContrast) ?? .white)
}
.frame(width: 3)
VStack(alignment: .leading) {
Text("\(reply.sender.firstName) \(reply.sender.lastName != nil ? "\(reply.sender.lastName!)" : "")")
.if(mainMessage.isOutgoing) {
$0.foregroundColor(.white)
} else: {
$0.foregroundColor(self.replyUsernameColor?.withLuminosity(colorSchemeContrast) ?? .white)
}
Group {
switch reply.content {
case let .text(info):
Text(info.text.text)
case let .photo(info):
makePreviewLabel(info.caption.text, icon: "photo")
case let .video(info):
makePreviewLabel(info.caption.text, icon: "video")
case let .document(info):
makePreviewLabel(info.caption.text, icon: "doc.text")
default:
Text(Constants.unsupportedMessage)
}
}
.if(mainMessage.isOutgoing) {
$0.foregroundColor(.white.darker(by: 50))
}
.if(!mainMessage.isOutgoing) {
$0.foregroundStyle(.secondary)
}
}
}
}

@ViewBuilder
var replyView: some View {
if let reply = message.first!.replyToMessage {
Button {
SystemUtils.post(notification: .scrollToMessage, with: reply.id)
} label: {
HStack {
Capsule()
.if(mainMessage.isOutgoing) {
$0.fill(.white)
} else: {
$0.fill(Color(fromUserId: reply.sender.id))
}
.frame(width: 3)
VStack(alignment: .leading) {
// swiftlint:disable line_length
Text("\(reply.sender.firstName) \(reply.sender.lastName != nil ? "\(reply.sender.lastName!)" : "")")
.if(mainMessage.isOutgoing) {
$0.foregroundColor(.white)
} else: {
$0.foregroundColor(Color(fromUserId: reply.sender.id))
}
Group {
switch reply.content {
case let .text(info):
Text(info.text.text)
case let .photo(info):
makePreviewLabel(info.caption.text, icon: "photo")
case let .video(info):
makePreviewLabel(info.caption.text, icon: "video")
case let .document(info):
makePreviewLabel(info.caption.text, icon: "doc.text")
default:
Text(Constants.unsupportedMessage)
}
}
.if(mainMessage.isOutgoing) {
$0.foregroundColor(.white.darker(by: 50))
}
.if(!mainMessage.isOutgoing) {
$0.foregroundStyle(.secondary)
}
}
}
replyLabel(reply: reply)
}
.buttonStyle(.plain)
.frame(height: 30)
.onAppear {
guard let reply = message.first!.replyToMessage else { return }
Task {
let replyUserId = reply.sender.id
self.replyUsernameColor = try await Color(from: replyUserId) ?? Color(fromUserId: replyUserId)
}
}
}
}
}
Loading