From e1dd6e15a99706fede43c7c38ae693acb52bad78 Mon Sep 17 00:00:00 2001 From: Chrys <53332481+ChrAlpha@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:14:41 +0800 Subject: [PATCH] fix(web): unify channel icon fallbacks (#397) --- apps/web/public/channels/feishu.png | Bin 669 -> 0 bytes apps/web/public/channels/matrix.svg | 5 --- apps/web/public/channels/telegram.webp | Bin 6724 -> 0 bytes .../web/src/components/channel-icon/index.vue | 18 +++++++++-- .../src/utils/channel-icon-fallback.test.ts | 29 ++++++++++++++++++ apps/web/src/utils/channel-icon-fallback.ts | 20 ++++++++++++ internal/handlers/file_embed.go | 4 +-- packages/icons/src/index.ts | 1 + 8 files changed, 66 insertions(+), 11 deletions(-) delete mode 100644 apps/web/public/channels/feishu.png delete mode 100644 apps/web/public/channels/matrix.svg delete mode 100644 apps/web/public/channels/telegram.webp create mode 100644 apps/web/src/utils/channel-icon-fallback.test.ts create mode 100644 apps/web/src/utils/channel-icon-fallback.ts diff --git a/apps/web/public/channels/feishu.png b/apps/web/public/channels/feishu.png deleted file mode 100644 index 73a55287721ed08c3cde07feb2dde6faa6eb715f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 669 zcmV;O0%HA%P)e~Pzk^tPgOn(0rE|?Qz z{|PskYBt)JPnX40krbS$+zs~;` zdZmk}|DLx0aC*4W-2Zcz|H8-M?(XsK)N50MKokaG7F-0@09Q0Gm6kUui%P;Y?e_nFREwgqdq8&VEANb7cJ`U`p4CxN z%9QzkT3Q9>Tt>FFswSpdlaZZnhU`hzy?|1af=UfYxw49K24(A1c_{}oWJgkpdseR` zA$Z$v&b>Z3sc_D5T7&DG;czsb@Yklk2s@m+y|aS4#ZtHLN9)uzoz6trJCt=E9)pIu zkM+bsjDg%gkn@~k403Df7-3gjbhoDS5~P?Kj1>VQf;=9!o;2!V^0bo>q|Btz*(Qb0mx!*nnKYp=&~R(O~l#nr6lBRbA$B^mj}1j zj6Wv#{mQ^3Ij#`WX|_{5dW;d0^)9BW9lxvVwA+wqm}Qv{_JNF9$l?WnUnGhwJ;!6yIjZR00000NkvXXu0mjf D0N72W diff --git a/apps/web/public/channels/matrix.svg b/apps/web/public/channels/matrix.svg deleted file mode 100644 index 979fc713..00000000 --- a/apps/web/public/channels/matrix.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/web/public/channels/telegram.webp b/apps/web/public/channels/telegram.webp deleted file mode 100644 index 3afc19e3868ffdc9f365680a711948d0db305dab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6724 zcmV-K8oT9ENk&FI8UO%SMM6+kP&iC48UO$ zJG@-?OFYe)44I9XK66Q#wR?6^mO{pbYmBqQC{P7hO!A)SM22)H=maO+vda$tv|aoE z6x&i>{XWB>cg@))UDaK-$0fraVq#u_H?VWgKE1oE{#D^W+Gy-@$5PxKPKcinjVv13 z3)d6j?s`+4J-DP0y1SE&`(#8O@dEDd?v}ZZ&*1J(#Nq*n=A^i5;}8;!?ZrF*o|6%| zEZUK6tG1g`k~a)7UfsJUuJfK)X82`Df*paQa1?X|KyoB0*gOCe4@<+zsf>(_$f#8a zbHBhh?&}n=Z9Cp($6wpFZQHhO+il9(wrwl;SKA!Y)Y)o#jlX9C=vv!<+IH^$HZsm$ z+P3%6wr%fY+lcmBbN&C{%pIKAdyExTu2`yDb+G%`))mZ*(}kB{+nljG8&_deuF~7~ z46eZFY;^pMe-WE<_%@5QQZU2S8mZM&UWn-By5 zEE{XO+U{ICwUrdt{|P{d=C}U{+PS(;*P#suw`VgfZk3yR$uUnIgR#8bju~(W!6DN} za>#~5HXX9(5a>JP-^LraXNgB@PsUH-aC;qsbI9{He8utBxJi~p!mXa2=D%9sp5H_< zaW29STz6?>ecO{f$GvQMKRaaDAzS@QI%MA=zug4wkZV>ca=Jn7iJ{~hUxHA)Qad;X z)y6(^2;MRDejOdMX!%s{uvgvOxb|4vp5B>Lfz_3>x>4=f>yGJi$Zo&pjyZ75faASy z^^{<*R*yV;dS~8;)lGY?V`lo^bjZ$o9QMe3mvNyIN2IkSjW%59Cbfs?d)Fak%iHRZ zins6n&UgPs*38k}cTYQ{v ztt0gVy>WZ??))#M6BScOf8sg%A$QEc1~ai>C7uo1<8gD~ci`>@PA%OI8km|CX8cpahjdaJy#3}ZvMTIRdennCK@L(9-MgssJ*m0lH~v@3~a z1FK!#5WH47SZAT9hZjVKak0#=4MA*`c{h%0!1I60U^fJ_MLKn^DQsqc`#-Dk6UuVQ z8v^=|mbv?Dz_cfZmOHc|ur0G-v4K#TS;r!DivYYW@@{?}F_5fakv~04mM zYs)T#*hjDq`_XKOkjouEU({h^%{worw+uPZ`VT4*{x_y>LkgTF* zvBY7NTI|MW17zmO+QMtYNd2T8M+N8a7E2mNtHmNldn7RKx`&T5j9E8(Z%#lOt$W2V z5V>2;4aO-9^U!c1dX2WpN<4v>qm^fa2)1yWGebr8+kArwc%J@@(co5}>_L5d23>S&BfuXBC^jX6t4J4{hJ&!boersrmd_%Xt#~(&2 z=|{hTuX@AGIvSc5e|D?Qg#bE#=z zQaRx@vJ#KxPE007jorFzeKhy+WO6i6<5XSrtmkBcG^31~EUmdSDME91B;RD}VK7%U z4N1~qFtt#zxu)r9qNkXU8d&|zrs?0kM5*begI3Q#hkMCVWA9h)|C%PJ3r!<-Y+p4k z<9Z46>~pGRD3@N+)O1><=A1okXnJ)4NuWv^%I_skO<$~#)k8>W*Hee4ssFEzhHv(g zr{N4sKsC3osfLzxG|fmMJwtm{L&KkYNmSE=O%)z^?ljUf zR6rFpgw+%zQceAu>Yko4)fAFShGrPnJtfyF8Iq~8N2j_~D0)gp(#gt-_Uo3ipRb%y z3#YY9&rmWYrL6pl?i9(nm6Pg=yYg!dZKV|@)Ahy5-B5Pn%5mzBp$JWzdLI_%zYSVu zlY!Q#tLJ5`SSRox5t@~Y?3FIeQ8F!fKp$GoMm0fY^-EBvbzI8=QV8r zZY&zQU(%G4iIFf*j2$!`s=cllS=FDvV0pUa8(%qvqoM%*ZYYX(r>#CjVldGcKZI3LeWmc52rG&TzEElR!;8dZcH9B;mM@f{$+Wk? z-VH;Cw)zx_)x3;_M$yO1P*elxHY_QM{y=5u(YDcunHCph51kSBSHLjn6vtSH()ENbf##(LIyw^;5*V>G<#8q>tYoI_SCyzd;;o*BS@3tc$FMaaZDyb`_wIe64JZvnDaPjyxh(Q zi~+tP&3W@znZUMqlR*{M#qkv99aw5c?Xso_ZfWfas<3*-E2KG~Jax;T1UNJ5f+{5H z%vefNn1txat2KX92y?6}12tLL_G2kc6hJ&_+o&55>M&)CDvvA2%A*_Lc zLseLqu?o2h79;m*{V5Rgo+nHXT&N0Zag3&rb|5jzs9iP`VQ>4(+hJ#V78~`qNjN@r zORoz?=o@wnTng#Jhp$Bf1%!so9aqBE7eBE$3@POAh<;>U& z(21+}ImAM(*9&^Ev@fkbd!8}kx80N*ap1}uc6DZM9vFDx@kLamU%>R>xqRb+MjZFL zVzZl2G;rlfp@^7m*wK+GP9Ea*gXK={srUZ)#Pg4XW&OZr*3nlsc5+h|vfm*V;^o|e zF#Gc6+>F)zL=eyWT;iV}K^+6ntA<}_Y`2VIs*KiWo zt-pjo$#A~8LGq#7fh2~*H4U=d8b&NMBjGV&D9)2yLVWkUV8`j<>wI(lMU#VA_GE3y za(f%`MWONLuE*_8oS)_^n9n!chqo+1EWfxcvd2(K;4+U1stb63Hs8a<`9n#;aM|PtKy`VC}9Hd(q6_VohSZtntvG7c;)O$1xl= z>0$3~;{L^yyWmdEd>`)8amf(lnGpwDfl16gp`i#A55<+Yjrec=A`h!s`0`W4db9&z zD=?)}$_RoUy6T&Y(+J>;pDwFe=*yowt!QHYO>DVzCg)TKz@4^#dJ7>u?Pbl<0MR%o zvF}PCY`Jv5059hn@O_}TFRr|%AcQ?mc=BDK15ee`JsrgU-75B6{G4l^`EAzv=DgX9 zLKs`TNxu9^wKz!H?@k=P1Fm4V$)NyTu30X~5lkoL- zw&UokfKhf}BTo0566x$Fb?s*? zVRh!^z6K0Q0()_n?TPz1Z;VfRrfXIUyqb;`!rC`XQ%geo3C10W{}5FRPkIpPA=Iq4 zp(Lozg2Km$K}qN!S?3^tuH+6+dPveUQnTF556YV1+FBC)i(v?0r!_6Xm!2Kz>8V*k z@q?tW^fnSm61=Xl=yApgVdIg8PkL6Q=l`0;`(qrRkI+|=@X?OcMi9rs_fcWJsF{g? z3!pgUc_iTzf{~Lju7gjS9XWZWW>ToSc@e?^cm4A(Fx|zpCy!7DD?5DB^F?%O>LvwB z`bz-p_P-xs@6+Xflasaxriq03q~@VCvuY>lpffQTIygJWZhre2rn~3@ore?59VGlx zb5_)0^^+tWaz1!2Pe*X?4or7Z2cKol2jOfq&_qCH%B1D&14)v6g;RRiZ9ic-**(^q zOn1|OqDFmumgYuT?M&G4wF7hoQjrAlVATj~=+@jtX zps!#`eei0Wm2yPdfn?R|mL{md@>T?-X^PUb=MzbiJeccm{&IrKOgxihRZsT%KVe-& z1PV$7rTJT$;9VmD>s!H*yR*=K%GNO!cI*0Z$Bqkzu-;JF9t@mtEh@534Qw|=0wa}eGjS-U@ektsqHY9sFrs1e=$ zMSX;}jIyV(uDG5h|H6wiyMV|3EN<9sM-X1;b0Q!;frye{_@sygf$3mH`v(DDbAJ(( z9bH6};tVjZ+MN(L?CByPMT*tWY=|huudwi9&pQz0?4%_EQXD}o}$m@55ncSXl{M1ccQvmhdDB6TtWstSD!y;TPeN zvG`5Y;BX7907)kT!gL+05#|aQRzdZ!9)nczgQAY%_Dq_5OK7C^sCJ78$TG+)@&k969T_`&q z3bPdsT5jeG$wx5alWHP%C^D6R&=P{-!+7?q1s(XL`gSKgV+aXtqHYHQPRkEb!!I;` zoixH+fQ07Bnw!2TXbOuje8Qx(UKejCDAkWap)H#pc5GM@LjgV^He$!p42OlL`3d`0 zp8}Lnb&XHBOFPS7Kv0-8U}!GTF_jI7E4AXwB%RJsgq_gPv_M(g_LqavX-RAFg*n$u zK!_-KXgUOC#%&vjhCLmhaNoiSW=lokMuZyx8l-62PB;vi30sAq^IRu zd>BEbBZVi@OQ)ESG>So@s=_Trg{SA7d}qjdY-Xb$RVTO z@5C?*yMQ9gVcM$0g5j*ymms6h!nvk|%v}H$Sx>^x`6{Zs;0g6bERpX@XCVVDvK+1} zZ{aEF&WD$!&C5Vx)&fR0Qd>#w!BLpLLYIYvrW789Kt`6s4do8pxH49Be5VLYo&D_x z)~s9tjclTCWi6AFL1pk+NFhgymV?554mPq}*9z|DqVZ5gmwDb`$*imVGT_KNR6-WcgbMdr|HbTo%sE;y0$!ggP2NvIl!^JW`qSmv-Ppx)MvL5=UmP zWG28@j`7AHuw`>W(G$=!lOm3mEDwcnICBGp-Pv=afyz{yUtYn^!$;V~kq<9InR_2X zic;4Cy6h;13QI3=BBkFA%+$AGq`1{bd z$!X{AVCMb;CFQ!s*tokN@PL-3e>7bZDO)fpN~AMAhJ|ev=*wWYGj|W16nECV3Ilb_ z09xk$yJgn|;Q=T``t>1eJ)}c-Xo5(??hBz6P>SLs8YZw(X~R^IXYM~%KeJUJ6P-Y% zxWF(oX+3O}AY}tBODA9zGj|iHp7NAHdDCvfTA}&7q#) z5UeUUUxBN-+FLj*=Co+PKvi3foQGXaWhB4I@Ngeq)z$v8u0dCS*u1VgGE84qBj+K5 zhNf6u9vtCm0J$4$PJZ%UuFTo6ueo!kD(F0Xh!wUEADle>2V_-MRUX_a(wSr=?Be>P zK~Hbc)rxD6qD?Dq3A0ob)I!Na6jr+7?e>(B8PjvCh0o$?M#A8pK&(1% zUrQI6fWgXVZ=bn&dL~uy^v;c|huq_tDzbD2){%!V(*#d<&*C?xS`_>s!qclMf~U!n zt%dbuVO)yf?Hc4Y6ENw4r-$EAn-}u*QflC75)E~+zO?e-X(Tc5APeiv${T5c2e_tI zb2HaZ9za6m!AkM+t2ov>)6wApDIM?tVcXtVcODd_18!Ww+BZCSp9*-;v#gEoJm6xrNeYVveOrWNC$d%~&?Si#L`=Xc&dQr8}W4&4@PIE=B*c zc15hQ;6D18hBxDkrE&N2(!E&vQ;cz=^FZiltDo6efMEedH_sWlC;IwbENG4`7L3n( zL>&E{z5Sh=1?_Rgf~nT)l0u))es%d-Fc?!Tn9ugJBKkek5o0YvJh5Q8RjZ@#ANJ2! zV!$V66|a!-BpM zz6fDB@5OT-#tnC2uYp13LOu%^5wrEZFtBtH$^ufXumGWpBN*NruwXS#SU_g(NDQ&y zL*?}`!h&_`vBfYyjODbm6htRW{r)b7de-m5LKkOf0g*|IFMq~>lOmD^`!PWaIt@?_ z2A%3?&;=nLSZa+FE*Scy$>LKi&|+X@S02MZMHiK{pvC|#5Zb=-1_a@=XfX~yTC6Z1 zZ-nBz$nq-mXz^WKK1d=UX)1^o>yV=biKg-!1m(Q1BtwfvxY1yk9Ib`WY*-LS7aK5J z{jd8%fNo0{A0S4Hjw2&b>B5(#=AcE3on@yGtjoQO77S3)Hij%^5wzrJd%75b6D_8> zD-fYO^>bP*!{~bZnTr74$(Q1xdydc+nYIADw1v=~XH=3vAk$Elw%$R9ZuRmUO9b?G zwAD>h09cyBlXOUfu-=Zj`k_KoM3$3gPw&hK@9k)65F#}7_x^iDkWZ1VaIzSM#%akS zeMI_Y7g^#8vXQBD3i;=h- z%gLj`&#+O~G}Ect7GshH#w55)iak<5ph-a?i+6WA1;%FUb@8?i#F3&tM5Guy=)*8h zM{cA;g5#~_cvwg=M_QMNVU98T+1BF7qeDT8wbp@W+f8Bg(i~H=I8TZR|DA^vw}{(* z3M2VuWRdoB(%L00@HT1DNion`9AWul++bACZIx~}CM|-GNQ+4GxJ{1Z|9gz`skPNZ zDQX=E{Lhw{H1ij~3Xa^4VXNgwibN8W9E9dPxIAgKeT%e~NwG; {{ fallback }} @@ -24,7 +26,9 @@ import { Wechatoa, Wecom, Matrix, + Misskey, } from '@memohai/icon' +import { channelIconFallbackText } from '@/utils/channel-icon-fallback' const channelIcons: Record = { qq: Qq, @@ -37,7 +41,7 @@ const channelIcons: Record = { wechatoa: Wechatoa, wecom: Wecom, matrix: Matrix, - // misskey: Misskey, + misskey: Misskey, dingtalk: Dingtalk, } @@ -50,11 +54,19 @@ const props = withDefaults(defineProps<{ defineOptions({ inheritAttrs: false }) +const normalizedChannel = computed(() => + props.channel.trim().toLowerCase(), +) + const iconComponent = computed(() => - channelIcons[props.channel], + channelIcons[normalizedChannel.value], ) const fallback = computed(() => - props.channel.slice(0, 2).toUpperCase(), + channelIconFallbackText(props.channel), ) + +const fallbackStyle = computed(() => ({ + fontSize: typeof props.size === 'number' ? `${props.size}px` : props.size, +})) diff --git a/apps/web/src/utils/channel-icon-fallback.test.ts b/apps/web/src/utils/channel-icon-fallback.test.ts new file mode 100644 index 00000000..a6dd1db8 --- /dev/null +++ b/apps/web/src/utils/channel-icon-fallback.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { channelIconFallbackText } from './channel-icon-fallback' + +describe('channelIconFallbackText', () => { + it('returns empty text for empty channel keys', () => { + expect(channelIconFallbackText('')).toBe('') + expect(channelIconFallbackText(' ')).toBe('') + }) + + it('uses explicit built-in fallbacks for non-brand channels', () => { + expect(channelIconFallbackText('local')).toBe('LC') + expect(channelIconFallbackText('cli')).toBe('CLI') + expect(channelIconFallbackText('web')).toBe('Web') + }) + + it('normalizes casing and whitespace', () => { + expect(channelIconFallbackText(' Discord ')).toBe('DI') + }) + + it('creates initials for multi-part unknown channels', () => { + expect(channelIconFallbackText('custom-bridge')).toBe('CB') + expect(channelIconFallbackText('foo_bar')).toBe('FB') + }) + + it('uses the first two alphanumeric characters for simple unknown channels', () => { + expect(channelIconFallbackText('misskey')).toBe('MI') + expect(channelIconFallbackText('qq')).toBe('QQ') + }) +}) diff --git a/apps/web/src/utils/channel-icon-fallback.ts b/apps/web/src/utils/channel-icon-fallback.ts new file mode 100644 index 00000000..f5f4458a --- /dev/null +++ b/apps/web/src/utils/channel-icon-fallback.ts @@ -0,0 +1,20 @@ +const explicitChannelFallbacks: Record = { + cli: 'CLI', + local: 'LC', + web: 'Web', +} + +export function channelIconFallbackText(channel: string): string { + const normalized = channel.trim().toLowerCase() + if (!normalized) return '' + + const explicit = explicitChannelFallbacks[normalized] + if (explicit) return explicit + + const parts = normalized.match(/[a-z0-9]+/gi) ?? [] + if (parts.length > 1) { + return parts.slice(0, 2).map((part) => part[0]?.toUpperCase() ?? '').join('') + } + + return normalized.replace(/[^a-z0-9]/gi, '').slice(0, 2).toUpperCase() +} diff --git a/internal/handlers/file_embed.go b/internal/handlers/file_embed.go index 1f86770c..1656a7c4 100644 --- a/internal/handlers/file_embed.go +++ b/internal/handlers/file_embed.go @@ -23,9 +23,7 @@ var embeddedStaticRoutes = map[string]struct { assetPath string contentType string }{ - "/logo.png": {assetPath: "logo.png", contentType: "image/png"}, - "/channels/telegram.webp": {assetPath: "channels/telegram.webp", contentType: "image/webp"}, - "/channels/feishu.png": {assetPath: "channels/feishu.png", contentType: "image/png"}, + "/logo.png": {assetPath: "logo.png", contentType: "image/png"}, } func NewEmbeddedWebHandler(log *slog.Logger) (*EmbeddedWebHandler, error) { diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts index bc5b59e3..adc7ba00 100644 --- a/packages/icons/src/index.ts +++ b/packages/icons/src/index.ts @@ -51,6 +51,7 @@ export { default as Microsoft } from './icons/Microsoft.vue' export { default as MicrosoftColor } from './icons/MicrosoftColor.vue' export { default as Minimax } from './icons/Minimax.vue' export { default as MinimaxColor } from './icons/MinimaxColor.vue' +export { default as Misskey } from './icons/Misskey.vue' export { default as Mistral } from './icons/Mistral.vue' export { default as MistralColor } from './icons/MistralColor.vue' export { default as Moonshot } from './icons/Moonshot.vue'