diff --git a/CHANGES.md b/CHANGES.md index 9f6cebb64..cbd6a5899 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ - New route in Locale API to fetch a list of languages - Added option to force HTTPS connection to the Appwrite server (_APP_OPTIONS_FORCE_HTTPS) - Added Google Fonts to Appwrite for offline availability +- Added a new route in the Avatars API to get user initials avatar ## Bug Fixes diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index bc493f8cc..f2a3c7bbf 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -16,6 +16,7 @@ use BaconQrCode\Renderer\Image\ImagickImageBackEnd; use BaconQrCode\Renderer\RendererStyle\RendererStyle; use BaconQrCode\Writer; use Utopia\Config\Config; +use Utopia\Validator\HexColor; include_once __DIR__ . '/../shared/api.php'; @@ -385,7 +386,83 @@ $utopia->get('/v1/avatars/qr') $response ->addHeader('Expires', date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache ->setContentType('image/png') - ->send('', $writer->writeString($text)) + ->send($writer->writeString($text)) + ; + } + ); + +$utopia->get('/v1/avatars/initials') + ->desc('Get User Initials') + ->param('name', '', function () { return new Text(512); }, 'Full Name. When empty, current user name or email will be used.', true) + ->param('width', 500, function () { return new Range(0, 2000); }, 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('height', 500, function () { return new Range(0, 2000); }, 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) + ->param('color', '', function () { return new HexColor(); }, 'Changes text color. By default a random color will be picked and stay will persistent to the given name.', true) + ->param('background', '', function () { return new HexColor(); }, 'Changes background color. By default a random color will be picked and stay will persistent to the given name.', true) + ->label('scope', 'avatars.read') + ->label('sdk.platform', [APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER]) + ->label('sdk.namespace', 'avatars') + ->label('sdk.method', 'getInitials') + ->label('sdk.methodType', 'location') + ->label('sdk.description', '/docs/references/avatars/get-initials.md') + ->action( + function ($name, $width, $height, $color, $background) use ($response, $user) { + $themes = [ + ['color' => '#27005e', 'background' => '#e1d2f6'], // VIOLET + ['color' => '#5e2700', 'background' => '#f3d9c6'], // ORANGE + ['color' => '#006128', 'background' => '#c9f3c6'], // GREEN + ['color' => '#580061', 'background' => '#f2d1f5'], // FUSCHIA + ['color' => '#00365d', 'background' => '#c6e1f3'], // BLUE + ['color' => '#00075c', 'background' => '#d2d5f6'], // INDIGO + ['color' => '#610038', 'background' => '#f5d1e6'], // PINK + ['color' => '#386100', 'background' => '#dcf1bd'], // LIME + ['color' => '#615800', 'background' => '#f1ecba'], // YELLOW + ['color' => '#610008', 'background' => '#f6d2d5'] // RED + ]; + + $rand = rand(0, count($themes)-1); + + $name = (!empty($name)) ? $name : $user->getAttribute('name', $user->getAttribute('email', '')); + $words = explode(' ', strtoupper($name)); + $initials = null; + $code = 0; + + foreach ($words as $key => $w) { + $initials .= (isset($w[0])) ? $w[0] : ''; + $code += (isset($w[0])) ? ord($w[0]) : 0; + + if($key == 1) { + break; + } + } + + $length = count($words); + $rand = substr($code,-1); + $background = (!empty($background)) ? '#'.$background : $themes[$rand]['background']; + $color = (!empty($color)) ? '#'.$color : $themes[$rand]['color']; + + $image = new \Imagick(); + $draw = new \ImagickDraw(); + $fontSize = min($width, $height) / 2; + + $draw->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-600.ttf"); + $image->setFont(__DIR__."/../../../public/fonts/poppins-v9-latin-600.ttf"); + + $draw->setFillColor(new \ImagickPixel($color)); + $draw->setFontSize($fontSize); + + $draw->setTextAlignment(\Imagick::ALIGN_CENTER); + $draw->annotation($width / 1.97, ($height / 2) + ($fontSize / 3), $initials); + + $image->newImage($width, $height, $background); + $image->setImageFormat("png"); + $image->drawImage($draw); + + //$image->setImageCompressionQuality(9 - round(($quality / 100) * 9)); + + $response + ->addHeader('Expires', date('D, d M Y H:i:s', time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache + ->setContentType('image/png') + ->send($image->getImageBlob()) ; } ); \ No newline at end of file diff --git a/docs/references/avatars/get-initials.md b/docs/references/avatars/get-initials.md new file mode 100644 index 000000000..87dafd3af --- /dev/null +++ b/docs/references/avatars/get-initials.md @@ -0,0 +1,3 @@ +Use this endpoint to show your user initials avatar icon on your website or app. By default, this route will try to print your logged-in user name or email initials. You can also overwrite the user name if you pass the 'name' parameter. If no name is given and no user is logged, an empty avatar will be returned. + +You can use the color and background params to change the avatar colors. By default, a random theme will be selected. The random theme will persist for the user's initials when reloading the same theme will always return for the same initials. \ No newline at end of file diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index 8a14e38f1..11ab2968c 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -2551,15 +2551,10 @@ return k;} function J(k){k=k.replace(/rn/g,"n");let d="";for(let F=0;F127&&x<2048){d+=String.fromCharCode((x>>6)|192);d+=String.fromCharCode((x&63)|128);}else{d+=String.fromCharCode((x>>12)|224);d+=String.fromCharCode(((x>>6)&63)|128);d+=String.fromCharCode((x&63)|128);}}} return d;} let C=Array();let P,h,E,v,g,Y,X,W,V;let S=7,Q=12,N=17,M=22;let A=5,z=9,y=14,w=20;let o=4,m=11,l=16,j=23;let U=6,T=10,R=15,O=21;s=J(s);C=e(s);Y=1732584193;X=4023233417;W=2562383102;V=271733878;for(P=0;Pchar.charCodeAt(0)).reduce((a,b)=>a+b,0).toString();let themes=[{color:"27005e",background:"e1d2f6"},{color:"5e2700",background:"f3d9c6"},{color:"006128",background:"c9f3c6"},{color:"580061",background:"f2d1f5"},{color:"00365d",background:"c6e1f3"},{color:"00075c",background:"d2d5f6"},{color:"610038",background:"f5d1e6"},{color:"386100",background:"dcf1bd"},{color:"615800",background:"f1ecba"},{color:"610008",background:"f6d2d5"}];name=name.split(" ").map(function(n){if(!isNaN(parseFloat(n))&&isFinite(n)){return"";} -return n[0];}).join("")||"--";let background=themes[theme[theme.length-1]]["background"];let color=themes[theme[theme.length-1]]["color"];let def="https://ui-avatars.com/api/"+ -encodeURIComponent(name)+"/"+ -size+"/"+ -encodeURIComponent(background)+"/"+ -encodeURIComponent(color);return("//www.gravatar.com/avatar/"+ -MD5(email)+".jpg?s="+ -size+"&d="+ -encodeURIComponent(def));}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return date.format("Y-m-d",$value);}).add("date-time",function($value,date){return date.format("Y-m-d H:i",$value);}).add("date-text",function($value,date){return date.format("d M Y",$value);}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ +let i=B(Y)+B(X)+B(W)+B(V);return i.toLowerCase();};let size=element.dataset["size"]||80;let email=$value.email||$value||"";let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;let def="/v1/avatars/initials?project=console"+"&name="+ +encodeURIComponent(name)+"&width="+ +size+"&height="+ +size;return def;}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return date.format("Y-m-d",$value);}).add("date-time",function($value,date){return date.format("Y-m-d H:i",$value);}).add("date-text",function($value,date){return date.format("d M Y",$value);}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ (days?days+"d ":"")+ (hours?hours+"h ":"")+ (minutes?minutes+"m ":"")+ diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js index 654ac7dfc..9c312adc0 100644 --- a/public/dist/scripts/app.js +++ b/public/dist/scripts/app.js @@ -267,15 +267,10 @@ return k;} function J(k){k=k.replace(/rn/g,"n");let d="";for(let F=0;F127&&x<2048){d+=String.fromCharCode((x>>6)|192);d+=String.fromCharCode((x&63)|128);}else{d+=String.fromCharCode((x>>12)|224);d+=String.fromCharCode(((x>>6)&63)|128);d+=String.fromCharCode((x&63)|128);}}} return d;} let C=Array();let P,h,E,v,g,Y,X,W,V;let S=7,Q=12,N=17,M=22;let A=5,z=9,y=14,w=20;let o=4,m=11,l=16,j=23;let U=6,T=10,R=15,O=21;s=J(s);C=e(s);Y=1732584193;X=4023233417;W=2562383102;V=271733878;for(P=0;Pchar.charCodeAt(0)).reduce((a,b)=>a+b,0).toString();let themes=[{color:"27005e",background:"e1d2f6"},{color:"5e2700",background:"f3d9c6"},{color:"006128",background:"c9f3c6"},{color:"580061",background:"f2d1f5"},{color:"00365d",background:"c6e1f3"},{color:"00075c",background:"d2d5f6"},{color:"610038",background:"f5d1e6"},{color:"386100",background:"dcf1bd"},{color:"615800",background:"f1ecba"},{color:"610008",background:"f6d2d5"}];name=name.split(" ").map(function(n){if(!isNaN(parseFloat(n))&&isFinite(n)){return"";} -return n[0];}).join("")||"--";let background=themes[theme[theme.length-1]]["background"];let color=themes[theme[theme.length-1]]["color"];let def="https://ui-avatars.com/api/"+ -encodeURIComponent(name)+"/"+ -size+"/"+ -encodeURIComponent(background)+"/"+ -encodeURIComponent(color);return("//www.gravatar.com/avatar/"+ -MD5(email)+".jpg?s="+ -size+"&d="+ -encodeURIComponent(def));}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return date.format("Y-m-d",$value);}).add("date-time",function($value,date){return date.format("Y-m-d H:i",$value);}).add("date-text",function($value,date){return date.format("d M Y",$value);}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ +let i=B(Y)+B(X)+B(W)+B(V);return i.toLowerCase();};let size=element.dataset["size"]||80;let email=$value.email||$value||"";let name=$value.name||$value||"";name=(typeof name!=='string')?'--':name;let def="/v1/avatars/initials?project=console"+"&name="+ +encodeURIComponent(name)+"&width="+ +size+"&height="+ +size;return def;}).add("selectedCollection",function($value,router){return $value===router.params.collectionId?"selected":"";}).add("selectedDocument",function($value,router){return $value===router.params.documentId?"selected":"";}).add("localeString",function($value){$value=parseInt($value);return!Number.isNaN($value)?$value.toLocaleString():"";}).add("date",function($value,date){return date.format("Y-m-d",$value);}).add("date-time",function($value,date){return date.format("Y-m-d H:i",$value);}).add("date-text",function($value,date){return date.format("d M Y",$value);}).add("ms2hum",function($value){let temp=$value;const years=Math.floor(temp/31536000),days=Math.floor((temp%=31536000)/86400),hours=Math.floor((temp%=86400)/3600),minutes=Math.floor((temp%=3600)/60),seconds=temp%60;if(days||hours||seconds||minutes){return((years?years+"y ":"")+ (days?days+"d ":"")+ (hours?hours+"h ":"")+ (minutes?minutes+"m ":"")+ diff --git a/public/scripts/filters.js b/public/scripts/filters.js index 6f08193df..dacb9f053 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -3,7 +3,7 @@ window.ls.filter if (!$value) { return ""; } - + // MD5 (Message-Digest Algorithm) by WebToolkit let MD5 = function(s) { function L(k, d) { @@ -216,59 +216,68 @@ window.ls.filter let email = $value.email || $value || ""; let name = $value.name || $value || ""; - name = (typeof name !== 'string') ? '' : name; + name = (typeof name !== 'string') ? '--' : name; - let theme = name - .split("") - .map(char => char.charCodeAt(0)) - .reduce((a, b) => a + b, 0) - .toString(); - let themes = [ - { color: "27005e", background: "e1d2f6" }, // VIOLET - { color: "5e2700", background: "f3d9c6" }, // ORANGE - { color: "006128", background: "c9f3c6" }, // GREEN - { color: "580061", background: "f2d1f5" }, // FUSCHIA - { color: "00365d", background: "c6e1f3" }, // BLUE - { color: "00075c", background: "d2d5f6" }, // INDIGO - { color: "610038", background: "f5d1e6" }, // PINK - { color: "386100", background: "dcf1bd" }, // LIME - { color: "615800", background: "f1ecba" }, // YELLOW - { color: "610008", background: "f6d2d5" } // RED - ]; + // let theme = name + // .split("") + // .map(char => char.charCodeAt(0)) + // .reduce((a, b) => a + b, 0) + // .toString(); + // let themes = [ + // { color: "27005e", background: "e1d2f6" }, // VIOLET + // { color: "5e2700", background: "f3d9c6" }, // ORANGE + // { color: "006128", background: "c9f3c6" }, // GREEN + // { color: "580061", background: "f2d1f5" }, // FUSCHIA + // { color: "00365d", background: "c6e1f3" }, // BLUE + // { color: "00075c", background: "d2d5f6" }, // INDIGO + // { color: "610038", background: "f5d1e6" }, // PINK + // { color: "386100", background: "dcf1bd" }, // LIME + // { color: "615800", background: "f1ecba" }, // YELLOW + // { color: "610008", background: "f6d2d5" } // RED + // ]; - name = - name - .split(" ") - .map(function(n) { - if (!isNaN(parseFloat(n)) && isFinite(n)) { - return ""; - } + // name = + // name + // .split(" ") + // .map(function(n) { + // if (!isNaN(parseFloat(n)) && isFinite(n)) { + // return ""; + // } - return n[0]; - }) - .join("") || "--"; + // return n[0]; + // }) + // .join("") || "--"; - let background = themes[theme[theme.length - 1]]["background"]; - let color = themes[theme[theme.length - 1]]["color"]; + // let background = themes[theme[theme.length - 1]]["background"]; + // let color = themes[theme[theme.length - 1]]["color"]; let def = - "https://ui-avatars.com/api/" + + "/v1/avatars/initials?project=console"+ + "&name=" + encodeURIComponent(name) + - "/" + + "&width=" + size + - "/" + - encodeURIComponent(background) + - "/" + - encodeURIComponent(color); + "&height=" + + size; - return ( - "//www.gravatar.com/avatar/" + - MD5(email) + - ".jpg?s=" + - size + - "&d=" + - encodeURIComponent(def) - ); + return def; + // let def = + // "https://ui-avatars.com/api/" + + // encodeURIComponent(name) + + // "/" + + // size + + // "/" + + // encodeURIComponent(background) + + // "/" + + // encodeURIComponent(color); + // return ( + // "//www.gravatar.com/avatar/" + + // MD5(email) + + // ".jpg?s=" + + // size + + // "&d=" + + // encodeURIComponent(def) + // ); }) .add("selectedCollection", function($value, router) { return $value === router.params.collectionId ? "selected" : ""; diff --git a/tests/e2e/Services/Avatars/AvatarsBase.php b/tests/e2e/Services/Avatars/AvatarsBase.php index 45b252e68..206ea0c40 100644 --- a/tests/e2e/Services/Avatars/AvatarsBase.php +++ b/tests/e2e/Services/Avatars/AvatarsBase.php @@ -411,4 +411,95 @@ trait AvatarsBase return []; } + + + public function testGetInitials() + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'width' => 200, + 'height' => 200, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200, + 'height' => 200, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200, + 'height' => 200, + 'color' => 'ffffff', + 'background' => '000000', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png; charset=UTF-8', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200000, + 'height' => 200, + 'color' => 'ffffff', + 'background' => '000000', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200, + 'height' => 200, + 'color' => 'white', + 'background' => '000000', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/avatars/initials', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'name' => 'W W', + 'width' => 200, + 'height' => 200, + 'color' => 'ffffff', + 'background' => 'black', + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } } \ No newline at end of file