Updated avatar color selection logic (#2151)

* updated avatar color selection logic

* tweaks

* more tweaks

* formatting
This commit is contained in:
mp-bw 2022-10-26 12:34:54 -04:00 committed by GitHub
parent 505426cd6a
commit 5deba15373
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 63 additions and 26 deletions

View file

@ -14,7 +14,7 @@ namespace Bit.App.Controls
{ {
AccountView = accountView; AccountView = accountView;
AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool") AvatarImageSource = ServiceContainer.Resolve<IAvatarImageSourcePool>("avatarImageSourcePool")
?.GetOrCreateAvatar(AccountView.Name, AccountView.Email); ?.GetOrCreateAvatar(AccountView.UserId, AccountView.Name, AccountView.Email);
} }
public AccountView AccountView public AccountView AccountView

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Utilities;
using SkiaSharp; using SkiaSharp;
using Xamarin.Forms; using Xamarin.Forms;
@ -10,7 +11,8 @@ namespace Bit.App.Controls
{ {
public class AvatarImageSource : StreamImageSource public class AvatarImageSource : StreamImageSource
{ {
private string _data; private readonly string _text;
private readonly string _id;
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
@ -21,20 +23,21 @@ namespace Bit.App.Controls
if (obj is AvatarImageSource avatar) if (obj is AvatarImageSource avatar)
{ {
return avatar._data == _data; return avatar._text == _text;
} }
return base.Equals(obj); return base.Equals(obj);
} }
public override int GetHashCode() => _data?.GetHashCode() ?? -1; public override int GetHashCode() => _text?.GetHashCode() ?? -1;
public AvatarImageSource(string name = null, string email = null) public AvatarImageSource(string userId = null, string name = null, string email = null)
{ {
_data = name; _id = userId;
if (string.IsNullOrWhiteSpace(_data)) _text = name;
if (string.IsNullOrWhiteSpace(_text))
{ {
_data = email; _text = email;
} }
} }
@ -52,24 +55,24 @@ namespace Bit.App.Controls
private Stream Draw() private Stream Draw()
{ {
string chars; string chars;
string upperData = null; string upperCaseText = null;
if (string.IsNullOrEmpty(_data)) if (string.IsNullOrEmpty(_text))
{ {
chars = ".."; chars = "..";
} }
else if (_data?.Length > 1) else if (_text?.Length > 1)
{ {
upperData = _data.ToUpper(); upperCaseText = _text.ToUpper();
chars = GetFirstLetters(upperData, 2); chars = GetFirstLetters(upperCaseText, 2);
} }
else else
{ {
chars = upperData = _data.ToUpper(); chars = upperCaseText = _text.ToUpper();
} }
var bgColor = StringToColor(upperData); var bgColor = CoreHelpers.StringToColor(_id ?? upperCaseText, "#33ffffff");
var textColor = Color.White; var textColor = CoreHelpers.TextColorFromBgColor(bgColor);
var size = 50; var size = 50;
using (var bitmap = new SKBitmap(size * 2, using (var bitmap = new SKBitmap(size * 2,
@ -85,7 +88,7 @@ namespace Bit.App.Controls
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill, Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter, StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex()) Color = SKColor.Parse(bgColor)
}) })
{ {
var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2; var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2;
@ -97,7 +100,7 @@ namespace Bit.App.Controls
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill, Style = SKPaintStyle.Fill,
StrokeJoin = SKStrokeJoin.Miter, StrokeJoin = SKStrokeJoin.Miter,
Color = SKColor.Parse(bgColor.ToHex()) Color = SKColor.Parse(bgColor)
}) })
{ {
canvas.DrawCircle(midX, midY, radius, circlePaint); canvas.DrawCircle(midX, midY, radius, circlePaint);
@ -108,7 +111,7 @@ namespace Bit.App.Controls
{ {
IsAntialias = true, IsAntialias = true,
Style = SKPaintStyle.Fill, Style = SKPaintStyle.Fill,
Color = SKColor.Parse(textColor.ToHex()), Color = SKColor.Parse(textColor),
TextSize = textSize, TextSize = textSize,
TextAlign = SKTextAlign.Center, TextAlign = SKTextAlign.Center,
Typeface = typeface Typeface = typeface

View file

@ -5,19 +5,19 @@ namespace Bit.App.Controls
{ {
public interface IAvatarImageSourcePool public interface IAvatarImageSourcePool
{ {
AvatarImageSource GetOrCreateAvatar(string name, string email); AvatarImageSource GetOrCreateAvatar(string userId, string name, string email);
} }
public class AvatarImageSourcePool : IAvatarImageSourcePool public class AvatarImageSourcePool : IAvatarImageSourcePool
{ {
private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>(); private readonly ConcurrentDictionary<string, AvatarImageSource> _cache = new ConcurrentDictionary<string, AvatarImageSource>();
public AvatarImageSource GetOrCreateAvatar(string name, string email) public AvatarImageSource GetOrCreateAvatar(string userId, string name, string email)
{ {
var key = $"{name}{email}"; var key = $"{userId}{name}{email}";
if (!_cache.TryGetValue(key, out var avatar)) if (!_cache.TryGetValue(key, out var avatar))
{ {
avatar = new AvatarImageSource(name, email); avatar = new AvatarImageSource(userId, name, email);
if (!_cache.TryAdd(key, avatar) if (!_cache.TryAdd(key, avatar)
&& &&
!_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add. !_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add.

View file

@ -129,7 +129,8 @@ namespace Bit.App.Pages
{ {
if (useCurrentActiveAccount) if (useCurrentActiveAccount)
{ {
return new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync()); return new AvatarImageSource(await _stateService.GetActiveUserIdAsync(),
await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
} }
return new AvatarImageSource(); return new AvatarImageSource();
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Web; using System.Web;
@ -264,5 +265,36 @@ namespace Bit.Core.Utilities
{ {
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj)); return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj));
} }
public static string TextColorFromBgColor(string hexColor, int threshold = 166)
{
if (new ColorConverter().ConvertFromString(hexColor) is Color bgColor)
{
var luminance = bgColor.R * 0.299 + bgColor.G * 0.587 + bgColor.B * 0.114;
return luminance > threshold ? "#ff000000" : "#ffffffff";
}
return "#ff000000";
}
public static string StringToColor(string str, string fallback)
{
if (str == null)
{
return fallback;
}
var hash = 0;
for (var i = 0; i < str.Length; i++)
{
hash = str[i] + ((hash << 5) - hash);
}
var color = "#FF";
for (var i = 0; i < 3; i++)
{
var value = (hash >> (i * 8)) & 0xff;
color += Convert.ToString(value, 16).PadLeft(2, '0');
}
return color;
}
} }
} }

View file

@ -32,8 +32,9 @@ namespace Bit.iOS.Core.Utilities
{ {
throw new NullReferenceException(nameof(_stateService)); throw new NullReferenceException(nameof(_stateService));
} }
var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync()); var avatarImageSource = new AvatarImageSource(await _stateService.GetActiveUserIdAsync(),
await _stateService.GetNameAsync(), await _stateService.GetEmailAsync());
using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync()) using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync())
{ {
return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE);