pleroma-like client for Bluesky pl.hexmani.ac
bluesky pleroma social-media

Compare changes

Choose any two refs to compare.

+33
src/styles/nav.scss
···
+
@use "vars";
+
+
#nav {
+
box-shadow:
+
0px 1px 4px 0px rgba(0, 0, 0, 0.4),
+
0px 2px 7px 0px rgba(0, 0, 0, 0.3);
+
display: grid;
+
grid-template-rows: auto;
+
grid-template-columns: auto auto auto auto auto;
+
grid-column-start: span 5;
+
grid-row-start: span 1;
+
background-color: vars.$foregroundColor;
+
height: 3.5rem;
+
padding-right: 0;
+
width: 100%;
+
}
+
+
.center-nav {
+
grid-column: 3;
+
position: relative;
+
+
img {
+
transition-timing-function: ease-in-out;
+
transition-duration: 0.15s;
+
margin-top: 0.5rem;
+
height: 2.5rem;
+
filter: grayscale(1);
+
}
+
+
img:hover {
+
filter: grayscale(0);
+
}
+
}
+73
src/styles/routes/login.scss
···
+
@use "../vars";
+
+
.typeface {
+
max-width: 400px;
+
width: 75%;
+
}
+
+
.logo-crawl {
+
display: flex;
+
flex-direction: row;
+
align-items: center;
+
justify-content: center;
+
gap: 2rem;
+
+
img {
+
transition: filter 0.25s ease-in-out;
+
max-height: 48px;
+
width: 48px;
+
/* lol? */
+
filter: brightness(0) saturate(100%) invert(91%) sepia(100%)
+
saturate(0%) hue-rotate(184deg) brightness(103%) contrast(102%);
+
}
+
+
img:hover {
+
transition: filter 0.25s ease-in-out;
+
filter: brightness(0) saturate(100%) invert(33%) sepia(76%)
+
saturate(1839%) hue-rotate(198deg) brightness(103%) contrast(99%);
+
}
+
}
+
+
hr {
+
color: vars.$textColor;
+
border: none;
+
border-top: 1px solid vars.$textColor;
+
max-width: 90%;
+
}
+
+
/* Login menu */
+
+
.login,
+
.login > form {
+
display: flex;
+
flex-direction: column;
+
text-align: left;
+
line-height: 24px;
+
padding: 0.3em 0.5em 0;
+
+
label {
+
margin-bottom: 0.25rem;
+
}
+
+
input {
+
background-color: vars.$foregroundColor;
+
border: 0;
+
border-radius: 3px;
+
box-shadow:
+
0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset,
+
0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset,
+
0px 0px 2px 0px rgba(0, 0, 0, 1) inset;
+
color: vars.$textColor;
+
padding: 0.5rem;
+
}
+
+
button {
+
display: flex;
+
flex-direction: row;
+
justify-content: end;
+
align-items: center;
+
text-align: right;
+
align-self: flex-end;
+
border-radius: vars.$containerBorderRadius;
+
}
+
}
+4
static/bluesky.svg
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<svg width="600" height="530" version="1.1" xmlns="http://www.w3.org/2000/svg">
+
<path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" fill="#ffffff"/>
+
</svg>
+154
static/tangled.svg
···
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+
<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+
<svg
+
version="1.1"
+
id="svg1"
+
width="24.122343"
+
height="23.274094"
+
viewBox="0 0 24.122343 23.274094"
+
sodipodi:docname="tangled_dolly_face_only.svg"
+
inkscape:export-filename="tangled_logotype_black_on_trans.svg"
+
inkscape:export-xdpi="96"
+
inkscape:export-ydpi="96"
+
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+
xmlns="http://www.w3.org/2000/svg"
+
xmlns:svg="http://www.w3.org/2000/svg">
+
<defs
+
id="defs1">
+
<filter
+
style="color-interpolation-filters:sRGB"
+
inkscape:menu-tooltip="Fades hue progressively to white"
+
inkscape:menu="Color"
+
inkscape:label="Hue to White"
+
id="filter24"
+
x="0"
+
y="0"
+
width="1"
+
height="1">
+
<feColorMatrix
+
values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 "
+
type="matrix"
+
result="r"
+
in="SourceGraphic"
+
id="feColorMatrix17" />
+
<feColorMatrix
+
values="0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 "
+
type="matrix"
+
result="g"
+
in="SourceGraphic"
+
id="feColorMatrix18" />
+
<feColorMatrix
+
values="0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 "
+
type="matrix"
+
result="b"
+
in="SourceGraphic"
+
id="feColorMatrix19" />
+
<feBlend
+
result="minrg"
+
in="r"
+
mode="darken"
+
in2="g"
+
id="feBlend19" />
+
<feBlend
+
result="p"
+
in="minrg"
+
mode="darken"
+
in2="b"
+
id="feBlend20" />
+
<feBlend
+
result="maxrg"
+
in="r"
+
mode="lighten"
+
in2="g"
+
id="feBlend21" />
+
<feBlend
+
result="q"
+
in="maxrg"
+
mode="lighten"
+
in2="b"
+
id="feBlend22" />
+
<feComponentTransfer
+
result="q2"
+
in="q"
+
id="feComponentTransfer22">
+
<feFuncR
+
slope="0"
+
type="linear"
+
id="feFuncR22" />
+
</feComponentTransfer>
+
<feBlend
+
result="pq"
+
in="p"
+
mode="lighten"
+
in2="q2"
+
id="feBlend23" />
+
<feColorMatrix
+
values="-1 1 0 0 0 -1 1 0 0 0 -1 1 0 0 0 0 0 0 0 1 "
+
type="matrix"
+
result="qminp"
+
in="pq"
+
id="feColorMatrix23" />
+
<feComposite
+
k3="1"
+
operator="arithmetic"
+
result="qminpc"
+
in="qminp"
+
in2="qminp"
+
id="feComposite23"
+
k1="0"
+
k2="0"
+
k4="0" />
+
<feBlend
+
result="result2"
+
in2="SourceGraphic"
+
mode="screen"
+
id="feBlend24" />
+
<feComposite
+
operator="in"
+
in="result2"
+
in2="SourceGraphic"
+
result="result1"
+
id="feComposite24" />
+
</filter>
+
</defs>
+
<sodipodi:namedview
+
id="namedview1"
+
pagecolor="#ffffff"
+
bordercolor="#000000"
+
borderopacity="0.25"
+
inkscape:showpageshadow="2"
+
inkscape:pageopacity="0.0"
+
inkscape:pagecheckerboard="true"
+
inkscape:deskcolor="#d5d5d5"
+
inkscape:zoom="7.0916564"
+
inkscape:cx="38.84847"
+
inkscape:cy="31.515909"
+
inkscape:window-width="1920"
+
inkscape:window-height="1080"
+
inkscape:window-x="0"
+
inkscape:window-y="0"
+
inkscape:window-maximized="0"
+
inkscape:current-layer="g1">
+
<inkscape:page
+
x="0"
+
y="0"
+
width="24.122343"
+
height="23.274094"
+
id="page2"
+
margin="0"
+
bleed="0" />
+
</sodipodi:namedview>
+
<g
+
inkscape:groupmode="layer"
+
inkscape:label="Image"
+
id="g1"
+
transform="translate(-0.4388285,-0.8629527)">
+
<path
+
style="fill:#ffffff;fill-opacity:1;stroke-width:0.111183;filter:url(#filter24)"
+
d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z"
+
id="path4" />
+
</g>
+
</svg>
+29
src/base.tsx
···
+
import { RouteSectionProps } from "@solidjs/router";
+
import { Component, createSignal, onMount, Show } from "solid-js";
+
import { retrieveSession, loginState } from "./components/login";
+
import Navbar from "./components/navbar";
+
+
const Base = (props: RouteSectionProps<unknown>) => {
+
const [isLoading, setIsLoading] = createSignal(true);
+
+
onMount(async () => {
+
await retrieveSession();
+
if (loginState() && location.pathname === "/") {
+
window.location.href = "/dash";
+
}
+
setIsLoading(false);
+
});
+
+
return (
+
<Show when={!isLoading()} fallback={<></>}>
+
<>
+
<header>
+
<Navbar />
+
</header>
+
<main>{props.children}</main>
+
</>
+
</Show>
+
);
+
};
+
+
export default Base;
+153
src/components/login.tsx
···
+
import { Did, isHandle } from "@atcute/lexicons/syntax";
+
import {
+
configureOAuth,
+
createAuthorizationUrl,
+
deleteStoredSession,
+
finalizeAuthorization,
+
getSession,
+
OAuthUserAgent,
+
resolveFromIdentity,
+
resolveFromService,
+
Session,
+
} from "@atcute/oauth-browser-client";
+
import { Component, createSignal } from "solid-js";
+
import Container from "./container";
+
+
configureOAuth({
+
metadata: {
+
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
+
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
+
},
+
});
+
+
export const [loginState, setLoginState] = createSignal(false);
+
let agent: OAuthUserAgent;
+
+
const Login: Component = () => {
+
const [notice, setNotice] = createSignal("");
+
const [loginInput, setLoginInput] = createSignal("");
+
+
const login = async (handle: string) => {
+
try {
+
if (!handle) return;
+
let resolved;
+
document.querySelector(".submitInfo")!.removeAttribute("hidden");
+
document
+
.querySelector('button[type="submit"]')!
+
.setAttribute("disabled", "");
+
if (!isHandle(handle)) {
+
setNotice(`Resolving your service...`);
+
resolved = await resolveFromService(handle);
+
} else {
+
setNotice(`Resolving your identity...`);
+
resolved = await resolveFromIdentity(handle);
+
}
+
+
setNotice(`Contacting your data server...`);
+
const authUrl = await createAuthorizationUrl({
+
scope: import.meta.env.VITE_OAUTH_SCOPE,
+
...resolved,
+
});
+
+
setNotice(`Redirecting...`);
+
await new Promise((resolve) => setTimeout(resolve, 500));
+
+
location.assign(authUrl);
+
} catch (e: unknown) {
+
if (e instanceof Error) {
+
console.error(e);
+
setNotice(`${e.message}`);
+
} else {
+
console.error(e);
+
setNotice(`Unknown error, check console ยฏ\\_(ใƒ„)_/ยฏ`);
+
}
+
} finally {
+
document
+
.querySelector('button[type="submit"]')!
+
.removeAttribute("disabled");
+
}
+
};
+
+
return (
+
<>
+
<Container
+
title="Log in"
+
children={
+
<>
+
<div class="login">
+
<form name="login" id="login" onclick={(e) => e.preventDefault()}>
+
<label for="handle">Handle</label>
+
<br />
+
<input
+
type="text"
+
id="handle"
+
name="handle"
+
maxlength="255"
+
placeholder="soykaf.com"
+
onInput={(e) => setLoginInput(e.currentTarget.value)}
+
required
+
/>
+
<br />
+
<button type="submit" onclick={() => login(loginInput())}>
+
Login
+
</button>
+
</form>
+
<p class="submitInfo" hidden>
+
{notice()}
+
</p>
+
</div>
+
</>
+
}
+
/>
+
</>
+
);
+
};
+
+
const retrieveSession = async (): Promise<void> => {
+
const init = async (): Promise<Session | undefined> => {
+
const params = new URLSearchParams(location.hash.slice(1));
+
+
if (params.has("state") && (params.has("code") || params.has("error"))) {
+
history.replaceState(null, "", location.pathname + location.search);
+
+
const session = await finalizeAuthorization(params);
+
console.log("Finalizing authorization...", session);
+
const agent = new OAuthUserAgent(session);
+
console.log(await agent.getSession());
+
const did = session.info.sub;
+
+
localStorage.setItem("currentUser", did);
+
return session;
+
} else {
+
const currentUser = localStorage.getItem("currentUser");
+
+
if (currentUser) {
+
try {
+
console.log("Retrieving session...");
+
return await getSession(currentUser as Did);
+
} catch (err) {
+
deleteStoredSession(currentUser as Did);
+
localStorage.removeItem("currentUser");
+
throw err;
+
}
+
}
+
}
+
};
+
+
const session = await init().catch(() => {});
+
+
if (session) {
+
console.log("Retrieved session!", session);
+
agent = new OAuthUserAgent(session);
+
setLoginState(true);
+
}
+
};
+
+
const killSession = async (): Promise<void> => {
+
await agent.signOut();
+
setLoginState(false);
+
localStorage.removeItem("currentUser");
+
location.href = "/";
+
};
+
+
export { agent, killSession, Login, retrieveSession };
+7 -13
src/index.tsx
···
import { render } from "solid-js/web";
import "solid-devtools";
import { Route, Router } from "@solidjs/router";
-
-
import Login from "./routes/login";
-
-
const root = document.getElementById("root");
-
-
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
-
throw new Error(
-
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
-
);
-
}
+
import Splash from "./routes/splash";
+
import Base from "./base";
+
import Dashboard from "./routes/dashboard";
render(
() => (
-
<Router root={Login}>
-
<Route path="/" component={Login} />
+
<Router root={Base}>
+
<Route path="/" component={Splash} />
+
<Route path="/dash" component={Dashboard} />
</Router>
),
-
root!,
+
document.getElementById("root") as HTMLElement,
);
+12
static/oauth/client-metadata.json
···
+
{
+
"client_id": "https://pl.hexmani.ac/oauth/client-metadata.json",
+
"client_name": "Bluroma",
+
"client_uri": "https://pl.hexmani.ac",
+
"redirect_uris": ["https://pl.hexmani.ac/"],
+
"scope": "atproto transition:generic",
+
"grant_types": ["authorization_code", "refresh_token"],
+
"response_types": ["code"],
+
"token_endpoint_auth_method": "none",
+
"application_type": "web",
+
"dpop_bound_access_tokens": true
+
}
+58
src/components/miniProfile.tsx
···
+
import { Component, Match, Show, Switch, createResource } from "solid-js";
+
import { Client } from "@atcute/client";
+
import { agent } from "./login";
+
+
type MiniProfileProps = {
+
did: `did:${string}:${string}`;
+
};
+
+
async function getProfileDetails(did: `did:${string}:${string}`) {
+
const rpc = new Client({ handler: agent });
+
+
const res = await rpc.get("app.bsky.actor.getProfile", {
+
params: {
+
actor: did,
+
},
+
});
+
+
if (!res.ok) {
+
throw new Error(`Failed to fetch profile details: ${res.status}`);
+
}
+
+
return res.data;
+
}
+
+
const MiniProfile = (props: MiniProfileProps) => {
+
const [profileInfo] = createResource(agent.sub, getProfileDetails);
+
+
return (
+
<>
+
<Show when={profileInfo.loading}>
+
<p>loading...</p>
+
</Show>
+
<Switch>
+
<Match when={profileInfo.error}>
+
<p>Error: {profileInfo.error.message}</p>
+
</Match>
+
<Match when={profileInfo()}>
+
<div
+
class="mini-profile"
+
// todo: add banner fade
+
style={`background-image: linear-gradient(to bottom, rgba(15, 22, 30, 0.85)), url(${profileInfo()?.banner}); background-size: cover; background-repeat: no-repeat;`}
+
>
+
<img
+
src={profileInfo()?.avatar}
+
alt={`Profile picture for ${profileInfo()?.handle}`}
+
/>
+
<div class="mini-profile-info">
+
<p>{profileInfo()?.displayName}</p>
+
<p>@{profileInfo()?.handle}</p>
+
</div>
+
</div>
+
</Match>
+
</Switch>
+
</>
+
);
+
};
+
+
export default MiniProfile;
+30
src/styles/profile.scss
···
+
@use "vars";
+
+
.mini-profile {
+
display: flex;
+
flex-direction: row;
+
align-items: center;
+
gap: 1rem;
+
padding: 1rem;
+
margin-bottom: 1rem;
+
border-radius: vars.$containerBorderRadius;
+
+
img {
+
max-height: 64px;
+
box-shadow: 10px 5px 5px rgba(0, 0, 0, 0.2);
+
border-radius: 3px;
+
}
+
}
+
+
.mini-profile-info {
+
text-align: left;
+
display: flex;
+
flex-direction: column;
+
align-items: flex-start;
+
justify-content: center;
+
gap: 0.5rem;
+
+
p {
+
margin: 0;
+
}
+
}
+74
src/components/post.tsx
···
+
import RelativeTime from "@yaireo/relative-time";
+
import { Show } from "solid-js";
+
import type { Post } from "../types/post";
+
+
type PostProps = {
+
data: Post;
+
};
+
+
// todo: don't just copy FA svgs in from akko-fe
+
const BoostIcon = () => {
+
return (
+
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
+
<title>Boost</title>
+
<path
+
class=""
+
fill="#5dc94a"
+
d="M272 416c17.7 0 32-14.3 32-32s-14.3-32-32-32H160c-17.7 0-32-14.3-32-32V192h32c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l32 0 0 128c0 53 43 96 96 96H272zM304 96c-17.7 0-32 14.3-32 32s14.3 32 32 32l112 0c17.7 0 32 14.3 32 32l0 128H416c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8l-32 0V192c0-53-43-96-96-96L304 96z"
+
></path>
+
</svg>
+
);
+
};
+
+
const Post = (props: PostProps) => {
+
return (
+
<div class="post">
+
<Show when={props.data.context}>
+
<div class="post-context">
+
<img
+
src={props.data.context?.invoker.avatar}
+
alt={`Profile picture of ${props.data.context?.invoker.handle}`}
+
/>
+
<span class="post-context-user">
+
{props.data.context?.invoker.displayName}
+
</span>
+
<BoostIcon />
+
<span>reposted</span>
+
</div>
+
</Show>
+
<div class="post-content">
+
<img
+
class="post-avatar"
+
src={props.data.avatar}
+
alt={`Profile picture of ${props.data.handle}`}
+
/>
+
<div class="post-main">
+
<div class="post-header">
+
<div class="post-author">
+
<span>{props.data.displayName}</span>
+
<span class="post-author-handle">@{props.data.handle}</span>
+
</div>
+
<span class="post-time">
+
{new RelativeTime({ options: { style: "narrow" } }).from(
+
props.data.createdAt,
+
)}
+
</span>
+
</div>
+
<div class="post-body">{props.data.record.text}</div>
+
</div>
+
</div>
+
<div class="post-interactions">
+
<p>
+
{props.data.counts.replyCount}{" "}
+
{props.data.counts.replyCount === 1 ? "reply" : "replies"} |{" "}
+
{props.data.counts.repostCount}{" "}
+
{props.data.counts.repostCount === 1 ? "repost" : "reposts"} |{" "}
+
{props.data.counts.likeCount}{" "}
+
{props.data.counts.likeCount === 1 ? "like" : "likes"}
+
</p>
+
</div>
+
</div>
+
);
+
};
+
+
export default Post;
-1
src/components/postForm.tsx
···
import { agent } from "./login";
import { Client } from "@atcute/client";
import * as TID from "@atcute/tid";
-
import RichtextBuilder from "@atcute/bluesky-richtext-builder";
const PostForm: Component = () => {
const [notice, setNotice] = createSignal("");
+22 -1
src/routes/dashboard.tsx
···
+
import { createResource, For, Match, Show, Switch } from "solid-js";
import Container from "../components/container";
import { agent, killSession, loginState } from "../components/login";
import MiniProfile from "../components/miniProfile";
import PostForm from "../components/postForm";
+
import { createPostElements, getFollowingTimeline } from "../utils/posts";
+
import Post from "../components/post";
+
+
async function renderTimeline() {
+
const feed = await getFollowingTimeline();
+
return await createPostElements(feed.feed);
+
}
const Dashboard = () => {
if (!loginState()) {
location.href = "/";
}
+
const [feed] = createResource(renderTimeline);
+
return (
<>
<div id="sidebar">
···
children={
<div class="container-content">
<div class="dashboard-feed">
-
<p>No more posts</p>
+
<Switch>
+
<Match when={feed.loading}>
+
<p>Loading...</p>
+
</Match>
+
<Match when={feed.error}>
+
<p>Error while loading timeline: {feed.error}</p>
+
</Match>
+
<Match when={feed()}>
+
<For each={feed()}>{(item) => <Post data={item} />}</For>
+
<p>No more posts</p>
+
</Match>
+
</Switch>
</div>
</div>
}
+224
src/styles/components/post.scss
···
+
@use "../vars";
+
+
$currentColor: #1185fe;
+
+
.dashboard-feed {
+
font-size: 0.95rem;
+
display: flex;
+
flex-direction: column;
+
overflow: scroll;
+
p {
+
color: #8d8d8d;
+
}
+
max-width: 600px;
+
width: 100%;
+
min-width: 0;
+
+
@media (max-width: 850px) {
+
max-width: 500px;
+
}
+
+
@media (max-width: 768px) {
+
max-width: 100%;
+
margin: 0;
+
padding: 0;
+
}
+
}
+
+
.post {
+
display: flex;
+
flex-direction: column;
+
gap: 0.1rem;
+
margin: 0.5rem 0;
+
border-bottom: 1px solid #444;
+
min-width: 0;
+
width: 100%;
+
}
+
+
.post-context {
+
display: flex;
+
gap: 0.5rem;
+
padding-left: 2.5rem;
+
padding-bottom: 0.5rem;
+
max-height: 32px;
+
align-items: center;
+
text-align: left;
+
+
.post-context-user {
+
color: $currentColor;
+
}
+
+
span {
+
color: rgba(185, 185, 186, 0.5);
+
}
+
+
span:first-of-type {
+
margin-left: 0.5rem;
+
}
+
+
img {
+
max-height: 24px;
+
border-radius: 5px;
+
}
+
+
svg {
+
max-height: 16px;
+
border-radius: 5px;
+
}
+
+
@media (max-width: 850px) {
+
span:first-of-type {
+
margin-left: 0rem;
+
}
+
}
+
+
@media (max-width: 768px) {
+
padding-left: 2rem;
+
gap: 0.34rem;
+
+
span:first-of-type {
+
margin-left: 0.1rem;
+
}
+
}
+
}
+
+
.post-content {
+
display: flex;
+
flex-direction: row;
+
gap: 1rem;
+
padding-left: 1rem;
+
min-width: 0;
+
+
@media (max-width: 850px) {
+
gap: 0.75rem;
+
padding-left: 0.75rem;
+
}
+
+
@media (max-width: 768px) {
+
padding-left: 0.5rem;
+
gap: 0.5rem;
+
}
+
}
+
+
.post-main {
+
display: flex;
+
flex-direction: column;
+
gap: 0.5rem;
+
flex: 1;
+
min-width: 0;
+
overflow-wrap: break-word;
+
}
+
+
.post-avatar {
+
width: 48px;
+
height: 48px;
+
border-radius: 5px;
+
flex-shrink: 0;
+
+
@media (max-width: 768px) {
+
width: 48px;
+
height: 48px;
+
}
+
}
+
+
.post-header {
+
display: flex;
+
flex-direction: row;
+
align-items: flex-start;
+
justify-content: space-between;
+
text-align: left;
+
width: 100%;
+
min-width: 0;
+
gap: 1rem;
+
+
.post-author {
+
display: flex;
+
gap: 0.5rem;
+
align-items: baseline;
+
min-width: 0;
+
flex: 1;
+
overflow: hidden;
+
+
span {
+
white-space: nowrap;
+
overflow: hidden;
+
text-overflow: ellipsis;
+
}
+
+
.post-author-handle {
+
color: #1185fe;
+
}
+
}
+
+
.post-time {
+
color: #8d8d8d;
+
white-space: nowrap;
+
flex-shrink: 0;
+
margin-left: auto;
+
margin-right: 1rem;
+
}
+
+
@media (max-width: 850px) {
+
.post-author {
+
span {
+
max-width: 150px;
+
}
+
}
+
}
+
+
@media (max-width: 768px) {
+
flex-direction: row;
+
align-items: flex-start;
+
justify-content: space-between;
+
+
.post-author {
+
flex-direction: column;
+
align-items: flex-start;
+
gap: 0.25rem;
+
flex: none;
+
max-width: calc(100% - 80px);
+
+
span {
+
white-space: normal;
+
overflow-wrap: break-word;
+
word-break: break-word;
+
max-width: none;
+
overflow: visible;
+
text-overflow: clip;
+
}
+
}
+
+
.post-time {
+
align-self: flex-start;
+
}
+
}
+
}
+
+
.post-body {
+
text-align: left;
+
margin-top: 0.25rem;
+
margin-right: 1rem;
+
overflow-wrap: break-word;
+
word-break: break-word;
+
+
@media (max-width: 850px) {
+
margin-right: 0.75rem;
+
}
+
+
@media (max-width: 768px) {
+
margin-right: 0.5rem;
+
}
+
}
+
+
.post-interactions {
+
text-align: left;
+
margin-left: 1rem;
+
+
@media (max-width: 850px) {
+
margin-left: 0.75rem;
+
}
+
+
@media (max-width: 768px) {
+
margin-left: 0.5rem;
+
}
+
}
+7 -3
src/styles/main.scss
···
@use "./button";
@use "./container";
+
@use "./components/post";
@use "./nav";
@use "./profile";
@use "./routes/dashboard";
···
background-color: rgba(12, 17, 24, 1);
font-family: Arial, Helvetica, sans-serif;
margin: 0;
-
overflow: hidden;
}
main {
···
justify-content: center;
margin: 0 auto;
max-width: 75%;
+
min-width: 0;
+
width: 100%;
}
@media (max-width: 768px) {
main {
flex-direction: column;
-
max-width: 90%;
-
margin: 0 1rem;
+
max-width: 100%;
+
margin: 0;
+
padding: 0 0.5rem;
+
box-sizing: border-box;
}
}
+26
src/types/post.ts
···
+
import { AppBskyFeedPost } from "@atcute/bluesky";
+
import { ProfileViewBasic } from "@atcute/bluesky/types/app/actor/defs";
+
+
export type Post = {
+
avatar?: string;
+
context?: PostContext;
+
counts: PostCounts;
+
createdAt: Date;
+
displayName: string;
+
handle: string;
+
indexedAt: Date;
+
record: AppBskyFeedPost.Main;
+
};
+
+
type PostCounts = {
+
bookmarkCount?: number;
+
likeCount?: number;
+
quoteCount?: number;
+
repostCount?: number;
+
replyCount?: number;
+
};
+
+
type PostContext = {
+
invoker: ProfileViewBasic;
+
reason: string;
+
};
+91
src/utils/posts.ts
···
+
import { Client } from "@atcute/client";
+
import { agent } from "../components/login";
+
import { FeedViewPost } from "@atcute/bluesky/types/app/feed/defs";
+
import type { Post } from "../types/post";
+
import { is } from "@atcute/lexicons";
+
import { AppBskyFeedPost } from "@atcute/bluesky";
+
+
export async function getFollowingTimeline(
+
cursor: string = "",
+
limit: number = 50,
+
) {
+
const rpc = new Client({ handler: agent });
+
+
const res = await rpc.get("app.bsky.feed.getTimeline", {
+
params: {
+
cursor,
+
limit,
+
},
+
});
+
+
if (!res.ok) {
+
throw new Error(
+
`Failed to fetch user's following timeline: ${res.data.error}/${res.data.message}`,
+
);
+
}
+
+
return { feed: res.data.feed, cursor: res.data.cursor };
+
}
+
+
export async function createPostElements(feed: FeedViewPost[]) {
+
let elms: Post[] = [];
+
const seenCreators = new Set<string>();
+
+
feed.forEach((post) => {
+
if (is(AppBskyFeedPost.mainSchema, post.post.record)) {
+
const record = post.post.record as unknown as AppBskyFeedPost.Main;
+
const isReply = record.reply !== undefined;
+
const creatorDid = post.post.author.did;
+
+
// Skip replies from creators who already have a post in elms
+
if (isReply && seenCreators.has(creatorDid)) {
+
return;
+
}
+
+
if (post.reason) {
+
if (post.reason.$type === "app.bsky.feed.defs#reasonRepost") {
+
elms.push({
+
avatar: post.post.author.avatar,
+
context: {
+
invoker: post.reason.by,
+
reason: post.reason.$type,
+
},
+
counts: {
+
bookmarkCount: post.post.bookmarkCount,
+
likeCount: post.post.likeCount,
+
quoteCount: post.post.quoteCount,
+
repostCount: post.post.repostCount,
+
replyCount: post.post.replyCount,
+
},
+
createdAt: new Date(post.post.record.createdAt),
+
displayName:
+
post.post.author.displayName || post.post.author.handle,
+
handle: post.post.author.handle,
+
indexedAt: new Date(post.post.indexedAt),
+
record: record,
+
});
+
seenCreators.add(creatorDid);
+
}
+
} else {
+
elms.push({
+
avatar: post.post.author.avatar,
+
counts: {
+
bookmarkCount: post.post.bookmarkCount,
+
likeCount: post.post.likeCount,
+
quoteCount: post.post.quoteCount,
+
repostCount: post.post.repostCount,
+
replyCount: post.post.replyCount,
+
},
+
createdAt: new Date(post.post.record.createdAt),
+
displayName: post.post.author.displayName || post.post.author.handle,
+
handle: post.post.author.handle,
+
indexedAt: new Date(post.post.indexedAt),
+
record: record,
+
});
+
seenCreators.add(creatorDid);
+
}
+
}
+
});
+
+
return elms;
+
}
+23
.tangled/build.yaml
···
+
when:
+
- event: [ "push", "pull_request" ]
+
branch: [ "main" ]
+
- event: [ "manual" ]
+
+
engine: "nixery"
+
+
dependencies:
+
nixpkgs:
+
- bun
+
- nodejs
+
+
steps:
+
- name: "Install dependencies"
+
command: "bun install"
+
+
- name: "Build app"
+
command: "bun run build"
+
+
clone:
+
skip: false
+
depth: 50
+
submodules: true
+17 -4
package.json
···
{
-
"name": "vite-template-solid",
-
"version": "0.0.0",
-
"description": "",
+
"name": "bluroma",
+
"version": "0.1.0",
+
"description": "pleroma-like client for Bluesky",
+
"keywords": [
+
"atproto",
+
"bluesky",
+
"client"
+
],
+
"homepage": "https://tangled.org/@hexmani.ac/bluroma",
+
"bugs": {
+
"url": "https://tangled.org/@hexmani.ac/bluroma/issues"
+
},
+
"license": "AGPL-3.0-only",
+
"repository": {
+
"type": "git",
+
"url": "git+https://tangled.org/@hexmani.ac/bluroma.git"
+
},
"type": "module",
"scripts": {
"start": "vite",
···
"build": "vite build",
"serve": "vite preview"
},
-
"license": "MIT",
"devDependencies": {
"@types/bun": "^1.3.0",
"sass": "^1.81.0",
.tangled/build.yaml .tangled/workflows/build.yaml
+1 -1
static/oauth/client-metadata.json static/oauth-client-metadata.json
···
{
-
"client_id": "https://pl.hexmani.ac/oauth/client-metadata.json",
+
"client_id": "https://pl.hexmani.ac/oauth-client-metadata.json",
"client_name": "Bluroma",
"client_uri": "https://pl.hexmani.ac",
"redirect_uris": ["https://pl.hexmani.ac/"],
+3 -3
index.html
···
<!doctype html>
<html lang="en">
<head>
-
<link rel="icon" href="/favicon.png" type="image/png" />
+
<link rel="icon" href="favicon.png" type="image/png" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#1285FE" />
···
<p>Please enable
JavaScript to continue.</p>
</p>
-
<img src="/favicon.png" alt="Bluroma Logo" />
+
<img src="favicon.png" alt="Bluroma Logo" />
</div>
</noscript>
</body>
-
<script src="/src/index.tsx" type="module"></script>
+
<script src="src/index.tsx" type="module"></script>
</html>
+3 -3
src/routes/splash.tsx
···
import { Component } from "solid-js";
import "../styles/main.scss";
-
import typefaceLogo from "/logo.png?url";
-
import blueskyLogo from "/bluesky.svg?url";
-
import tangledLogo from "/tangled.svg?url";
+
import typefaceLogo from "/media/logo.png?url";
+
import blueskyLogo from "/media/bluesky.svg?url";
+
import tangledLogo from "/media/tangled.svg?url";
import Container from "../components/container";
import { Login, loginState } from "../components/login";
static/bluesky.svg static/media/bluesky.svg
static/logo.png static/media/logo.png
static/tangled.svg static/media/tangled.svg
+41
.tangled/workflows/deploy-main.yaml
···
+
when:
+
- event: ["push"]
+
branch: ["main"]
+
- event: ["manual"]
+
+
engine: "nixery"
+
+
dependencies:
+
nixpkgs:
+
- bun
+
- coreutils
+
- curl
+
- nodejs
+
+
environment:
+
SITE_PATH: "dist"
+
SITE_NAME: "bluroma"
+
WISP_HANDLE: "hexmani.ac"
+
+
steps:
+
- name: "Install dependencies"
+
command: "bun install --frozen-lockfile"
+
+
- name: "Build app"
+
command: "bun run build"
+
+
- name: "Deploy to Wisp"
+
command: |
+
curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
+
chmod +x wisp-cli
+
+
./wisp-cli deploy \
+
"$WISP_HANDLE" \
+
--path "$SITE_PATH" \
+
--site "$SITE_NAME" \
+
--password "$WISP_APP_PASSWORD"
+
+
clone:
+
skip: false
+
depth: 50
+
submodules: true