Skip to content

Commit

Permalink
Support for dynamic username coloring (#152)
Browse files Browse the repository at this point in the history
* dynamic username coloring and increased contrast support

* files in .pbxproj
  • Loading branch information
levochkaa authored Sep 17, 2022
1 parent 5d842a4 commit df07bdb
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 39 deletions.
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

0 comments on commit df07bdb

Please sign in to comment.