Introducing Damus Notedeck: a nostr browser
This splits notedeck into: - notedeck - notedeck_chrome - notedeck_columns The `notedeck` crate is the library that `notedeck_chrome` and `notedeck_columns`, use. It contains common functionality related to notedeck apps such as the NoteCache, ImageCache, etc. The `notedeck_chrome` crate is the binary and ui chrome. It is responsible for managing themes, user accounts, signing, data paths, nostrdb, image caches etc. It will eventually have its own ui which has yet to be determined. For now it just manages the browser data, which is passed to apps via a new struct called `AppContext`. `notedeck_columns` is our columns app, with less responsibility now that more things are handled by `notedeck_chrome` There is still much work left to do before this is a proper browser: - process isolation - sandboxing - etc This is the beginning of a new era! We're just getting started. Signed-off-by: William Casarin <jb55@jb55.com>
@@ -11,10 +11,11 @@ description = "A tweetdeck-style notedeck app"
|
||||
crate-type = ["lib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
base32 = { workspace = true }
|
||||
notedeck = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
eframe = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
egui_nav = { workspace = true }
|
||||
@@ -47,7 +48,7 @@ urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.13.0"
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
security-framework = "2.11.0"
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 266 KiB |
@@ -1,261 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg21"
|
||||
sodipodi:docname="damus-app-icon.svg"
|
||||
inkscape:version="1.4 (e7c3feb1, 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">
|
||||
<sodipodi:namedview
|
||||
id="namedview21"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.2800591"
|
||||
inkscape:cx="724.847"
|
||||
inkscape:cy="724.847"
|
||||
inkscape:window-width="1104"
|
||||
inkscape:window-height="771"
|
||||
inkscape:window-x="222"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg21" />
|
||||
<g
|
||||
id="g22"
|
||||
transform="translate(-6.40822,-11.4789)">
|
||||
<g
|
||||
filter="url(#filter0_dii_3010_339)"
|
||||
id="g1"
|
||||
transform="translate(96.40822,102.9789)"
|
||||
inkscape:label="logo-bg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 834,256.627 c 0,-9.782 0.004,-19.565 -0.056,-29.348 -0.049,-8.241 -0.144,-16.48 -0.368,-24.717 -0.484,-17.953 -1.543,-36.06 -4.736,-53.813 C 825.602,130.741 820.314,113.98 811.981,97.6166 803.789,81.5337 793.088,66.8168 780.32,54.0578 767.555,41.2989 752.834,30.6049 736.746,22.4179 720.366,14.0829 703.587,8.79697 685.558,5.55998 667.803,2.37199 649.691,1.315 631.738,0.829997 623.495,0.606998 615.253,0.512998 607.008,0.461998 597.22,0.401999 587.432,0.406999 577.644,0.406999 L 463.997,0 h -85 L 267.361,0.406999 c -9.807,0 -19.614,-0.005 -29.421,0.054999 -8.262,0.051 -16.52,0.145 -24.779,0.367999 C 195.167,1.315 177.014,2.37299 159.217,5.56498 141.164,8.80097 124.36,14.0849 107.958,22.4139 91.8354,30.6019 77.0825,41.2969 64.2906,54.0578 51.5007,66.8158 40.7798,81.5297 32.5728,97.6096 24.2169,113.981 18.9189,130.752 15.673,148.77 c -3.196,17.746 -4.255,35.847 -4.742,53.792 -0.222,8.238 -0.318,16.477 -0.368,24.717 -0.06,9.784 -0.563,21.937 -0.563,31.72 l 0.003,110.09 -0.003,85.909 0.508,112.429 c 0,9.796 -0.004,19.592 0.055,29.388 0.05,8.252 0.146,16.502 0.369,24.751 0.486,17.976 1.547,36.109 4.746,53.886 3.2449,18.032 8.5419,34.817 16.8908,51.201 8.208,16.106 18.9309,30.842 31.7218,43.619 12.7909,12.777 27.5398,23.485 43.6594,31.684 16.412,8.346 33.224,13.639 51.288,16.88 17.789,3.193 35.936,4.252 53.923,4.737 8.259,0.223 16.518,0.318 24.78,0.368 9.807,0.06 19.613,0.056 29.42,0.056 L 380.006,824 h 85.211 l 112.427,-0.004 c 9.788,0 19.576,0.005 29.364,-0.055 8.245,-0.05 16.487,-0.145 24.73,-0.368 17.96,-0.486 36.078,-1.546 53.841,-4.741 18.018,-3.241 34.789,-8.532 51.16,-16.873 16.092,-8.198 30.815,-18.908 43.581,-31.687 12.766,-12.775 23.466,-27.509 31.658,-43.612 8.338,-16.392 13.626,-33.185 16.866,-51.229 3.19,-17.77 4.248,-35.896 4.733,-53.865 0.223,-8.25 0.318,-16.5 0.367,-24.751 0.061,-9.796 0.056,-19.592 0.056,-29.388 0,0 -0.006,-110.444 -0.006,-112.429 v -85.999 c 0,-1.466 0.006,-112.372 0.006,-112.372 z"
|
||||
fill="url(#paint0_linear_3010_339)"
|
||||
id="path1"
|
||||
style="fill:url(#paint0_linear_3010_339)" />
|
||||
</g>
|
||||
<g
|
||||
id="g21"
|
||||
inkscape:label="center-logo"
|
||||
transform="translate(96.40822,99.978896)">
|
||||
<path
|
||||
d="M 343.319,671.664 C 240,748.442 240,152 240,152 c 206.638,45.258 413.278,90.517 413.276,189.925 -0.003,99.409 -206.637,252.961 -309.957,329.739 z"
|
||||
fill="url(#paint1_linear_3010_339)"
|
||||
stroke="#ffffff"
|
||||
stroke-width="30.3537"
|
||||
id="path2"
|
||||
style="fill:url(#paint1_linear_3010_339)" />
|
||||
<path
|
||||
d="m 240.68,255.493 135.608,68.759 -36.29,-143.247 z"
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.325424"
|
||||
stroke="#ffffff"
|
||||
stroke-width="6.07075"
|
||||
id="path3" />
|
||||
<path
|
||||
d="M 374.627,322.975 361.121,455.329 249.025,343.233 Z"
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.274576"
|
||||
stroke="#ffffff"
|
||||
stroke-width="6.07075"
|
||||
id="path4" />
|
||||
<path
|
||||
d="M 373.276,323.65 461.738,210.879 540.07,330.403 Z"
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.379661"
|
||||
stroke="#ffffff"
|
||||
stroke-width="6.07075"
|
||||
id="path5" />
|
||||
<path
|
||||
d="M 374.626,324.326 548.172,491.794 539.393,330.403 Z"
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.447458"
|
||||
stroke="#ffffff"
|
||||
stroke-width="6.07075"
|
||||
id="path6" />
|
||||
<path
|
||||
d="M 360.445,454.654 548.847,493.145 375.301,324.326 Z"
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.20678"
|
||||
stroke="#ffffff"
|
||||
stroke-width="6.07075"
|
||||
id="path7" />
|
||||
<path
|
||||
d="m 360.446,454.654 -86.435,99.941 189.752,22.959 z"
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.244068"
|
||||
stroke="#ffffff"
|
||||
stroke-width="6.07075"
|
||||
id="path8" />
|
||||
<path
|
||||
d="m 540.069,330.403 90.487,71.579 -39.841,-140.457 z"
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.216949"
|
||||
stroke="#ffffff"
|
||||
stroke-width="6.07075"
|
||||
id="path9" />
|
||||
<path
|
||||
d="m 360.702,460.732 c 3.356,0 6.077,-2.721 6.077,-6.078 0,-3.356 -2.721,-6.077 -6.077,-6.077 -3.357,0 -6.078,2.721 -6.078,6.077 0,3.357 2.721,6.078 6.078,6.078 z"
|
||||
fill="#ffffff"
|
||||
id="path10" />
|
||||
<path
|
||||
d="m 374.882,329.728 c 3.357,0 6.078,-2.721 6.078,-6.078 0,-3.356 -2.721,-6.077 -6.078,-6.077 -3.356,0 -6.077,2.721 -6.077,6.077 0,3.357 2.721,6.078 6.077,6.078 z"
|
||||
fill="#ffffff"
|
||||
id="path11" />
|
||||
<path
|
||||
d="m 539.905,336.225 c 3.356,0 6.077,-2.721 6.077,-6.077 0,-3.357 -2.721,-6.078 -6.077,-6.078 -3.357,0 -6.078,2.721 -6.078,6.078 0,3.356 2.721,6.077 6.078,6.077 z"
|
||||
fill="#ffffff"
|
||||
id="path12" />
|
||||
</g>
|
||||
</g>
|
||||
<defs
|
||||
id="defs21">
|
||||
<filter
|
||||
id="filter0_dii_3010_339"
|
||||
x="0"
|
||||
y="-3"
|
||||
width="844"
|
||||
height="847"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
id="feFlood12" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
id="feColorMatrix12" />
|
||||
<feOffset
|
||||
dy="10"
|
||||
id="feOffset12" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="5"
|
||||
id="feGaussianBlur12" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0"
|
||||
id="feColorMatrix13" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_3010_339"
|
||||
id="feBlend13" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_3010_339"
|
||||
result="shape"
|
||||
id="feBlend14" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
id="feColorMatrix14" />
|
||||
<feOffset
|
||||
dy="4"
|
||||
id="feOffset14" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="1"
|
||||
id="feGaussianBlur14" />
|
||||
<feComposite
|
||||
in2="hardAlpha"
|
||||
operator="arithmetic"
|
||||
k2="-1"
|
||||
k3="1"
|
||||
id="feComposite14"
|
||||
k1="0"
|
||||
k4="0" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0"
|
||||
id="feColorMatrix15" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="shape"
|
||||
result="effect2_innerShadow_3010_339"
|
||||
id="feBlend15" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
id="feColorMatrix16" />
|
||||
<feOffset
|
||||
dy="-3"
|
||||
id="feOffset16" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="2"
|
||||
id="feGaussianBlur16" />
|
||||
<feComposite
|
||||
in2="hardAlpha"
|
||||
operator="arithmetic"
|
||||
k2="-1"
|
||||
k3="1"
|
||||
id="feComposite16"
|
||||
k1="0"
|
||||
k4="0" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
|
||||
id="feColorMatrix17" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="effect2_innerShadow_3010_339"
|
||||
result="effect3_innerShadow_3010_339"
|
||||
id="feBlend17" />
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_3010_339"
|
||||
x1="42.405701"
|
||||
y1="800.86902"
|
||||
x2="803.62"
|
||||
y2="23.1313"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#1C55FF"
|
||||
id="stop17" />
|
||||
<stop
|
||||
offset="0.5"
|
||||
stop-color="#7F35AB"
|
||||
id="stop18" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#FF0BD6"
|
||||
id="stop19" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_3010_339"
|
||||
x1="224.823"
|
||||
y1="410.40201"
|
||||
x2="668.45203"
|
||||
y2="410.40201"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(0,3)">
|
||||
<stop
|
||||
stop-color="#0DE8FF"
|
||||
stop-opacity="0.780822"
|
||||
id="stop20" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#D600FC"
|
||||
stop-opacity="0.954338"
|
||||
id="stop21" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 286 KiB |
@@ -1,184 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="256mm"
|
||||
height="256mm"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
sodipodi:docname="damus.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5946522"
|
||||
inkscape:cx="406.11975"
|
||||
inkscape:cy="491.88416"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1060"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg5"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39361">
|
||||
<stop
|
||||
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
|
||||
offset="0"
|
||||
id="stop39357" />
|
||||
<stop
|
||||
style="stop-color:#d600fc;stop-opacity:0.95433789;"
|
||||
offset="1"
|
||||
id="stop39359" />
|
||||
</linearGradient>
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect255"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2119">
|
||||
<stop
|
||||
style="stop-color:#1c55ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2115" />
|
||||
<stop
|
||||
style="stop-color:#7f35ab;stop-opacity:1;"
|
||||
offset="0.5"
|
||||
id="stop2123" />
|
||||
<stop
|
||||
style="stop-color:#ff0bd6;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop2117" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2119"
|
||||
id="linearGradient2121"
|
||||
x1="10.067794"
|
||||
y1="248.81357"
|
||||
x2="246.56145"
|
||||
y2="7.1864405"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39361"
|
||||
id="linearGradient39367"
|
||||
x1="62.104473"
|
||||
y1="128.78963"
|
||||
x2="208.25758"
|
||||
y2="128.78963"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Background"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
sodipodi:insensitive="true">
|
||||
<rect
|
||||
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect61"
|
||||
width="256"
|
||||
height="256"
|
||||
x="-5.3875166e-08"
|
||||
y="-1.0775033e-07"
|
||||
ry="0"
|
||||
inkscape:label="Gradient"
|
||||
sodipodi:insensitive="true" />
|
||||
</g>
|
||||
<g
|
||||
id="g407"
|
||||
inkscape:label="Logo">
|
||||
<g
|
||||
id="layer2"
|
||||
inkscape:label="LogoStroke"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
|
||||
id="path253" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Poly">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
|
||||
id="path4648" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
|
||||
id="path9299" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
|
||||
id="path9301" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
|
||||
id="path9368" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
|
||||
id="path9370" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
|
||||
id="path9372" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
|
||||
id="path9374" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Vertices">
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path27764"
|
||||
cx="106.86934"
|
||||
cy="142.38014"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle28773"
|
||||
cx="111.54119"
|
||||
cy="99.221161"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle29091"
|
||||
cx="165.90784"
|
||||
cy="101.36163"
|
||||
r="2.0022209" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.4 KiB |
@@ -1,334 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="256mm"
|
||||
height="256mm"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||
sodipodi:docname="damus_rounded.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5946522"
|
||||
inkscape:cx="405.27892"
|
||||
inkscape:cy="543.17465"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1080"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect9"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,80,0,1 @ F,0,1,1,0,80,0,1 @ F,0,0,1,0,80,0,1 @ F,0,0,1,0,80,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect8"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect7"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect6"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect5"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect4"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect3"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect2"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<inkscape:path-effect
|
||||
effect="fillet_chamfer"
|
||||
id="path-effect1"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||
radius="0"
|
||||
unit="px"
|
||||
method="auto"
|
||||
mode="F"
|
||||
chamfer_steps="1"
|
||||
flexible="false"
|
||||
use_knot_distance="true"
|
||||
apply_no_radius="true"
|
||||
apply_with_radius="true"
|
||||
only_selected="false"
|
||||
hide_knots="false" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39361">
|
||||
<stop
|
||||
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
|
||||
offset="0"
|
||||
id="stop39357" />
|
||||
<stop
|
||||
style="stop-color:#d600fc;stop-opacity:0.95433789;"
|
||||
offset="1"
|
||||
id="stop39359" />
|
||||
</linearGradient>
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect255"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2119">
|
||||
<stop
|
||||
style="stop-color:#1c55ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2115" />
|
||||
<stop
|
||||
style="stop-color:#7f35ab;stop-opacity:1;"
|
||||
offset="0.5"
|
||||
id="stop2123" />
|
||||
<stop
|
||||
style="stop-color:#ff0bd6;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop2117" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2119"
|
||||
id="linearGradient2121"
|
||||
x1="10.067794"
|
||||
y1="248.81357"
|
||||
x2="246.56145"
|
||||
y2="7.1864405"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39361"
|
||||
id="linearGradient39367"
|
||||
x1="62.104473"
|
||||
y1="128.78963"
|
||||
x2="208.25758"
|
||||
y2="128.78963"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Background"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
sodipodi:insensitive="true">
|
||||
<path
|
||||
id="rect61"
|
||||
style="fill:url(#linearGradient2121);stroke-width:0.264583;opacity:1"
|
||||
inkscape:label="Gradient"
|
||||
d="m 80,-1.0775033e-7 h 96 A 80,80 45 0 1 256,80 v 96 a 80,80 135 0 1 -80,80 H 80 A 80,80 45 0 1 -5.3875166e-8,176 V 80 A 80,80 135 0 1 80,-1.0775033e-7 Z"
|
||||
inkscape:path-effect="#path-effect9"
|
||||
inkscape:original-d="M -5.3875166e-8,-1.0775033e-7 H 256 V 256 H -5.3875166e-8 Z" />
|
||||
</g>
|
||||
<g
|
||||
id="g407"
|
||||
inkscape:label="Logo">
|
||||
<g
|
||||
id="layer2"
|
||||
inkscape:label="LogoStroke"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
|
||||
id="path253" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Poly">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
|
||||
id="path4648" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
|
||||
id="path9299" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
|
||||
id="path9301" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
|
||||
id="path9368" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
|
||||
id="path9370" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
|
||||
id="path9372" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
|
||||
id="path9374" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Vertices">
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path27764"
|
||||
cx="106.86934"
|
||||
cy="142.38014"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle28773"
|
||||
cx="111.54119"
|
||||
cy="99.221161"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle29091"
|
||||
cx="165.90784"
|
||||
cy="101.36163"
|
||||
r="2.0022209" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 806 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 554 B |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 340 B |
|
Before Width: | Height: | Size: 912 B |
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M1 4.2C1 3.07989 1 2.51984 1.21799 2.09202C1.40973 1.71569 1.71569 1.40973 2.09202 1.21799C2.51984 1 3.07989 1 4.2 1H9.8C10.9201 1 11.4801 1 11.908 1.21799C12.2843 1.40973 12.5903 1.71569 12.782 2.09202C13 2.51984 13 3.07989 13 4.2V7.8C13 8.92013 13 9.48013 12.782 9.908C12.5903 10.2843 12.2843 10.5903 11.908 10.782C11.4801 11 10.9201 11 9.8 11H8.12247C7.70647 11 7.49847 11 7.29947 11.0409C7.12293 11.0771 6.95213 11.137 6.79167 11.219C6.6108 11.3114 6.44833 11.4413 6.12347 11.7012L4.53317 12.9735C4.25578 13.1954 4.11709 13.3063 4.00036 13.3065C3.89885 13.3066 3.80281 13.2604 3.73949 13.1811C3.66667 13.0899 3.66667 12.9123 3.66667 12.557V11C3.04669 11 2.73669 11 2.48236 10.9319C1.79218 10.7469 1.25308 10.2078 1.06815 9.51767C1 9.26333 1 8.95333 1 8.33333V4.2V4.2Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
Before Width: | Height: | Size: 969 B |
|
Before Width: | Height: | Size: 808 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "egui Template PWA",
|
||||
"short_name": "egui-template-pwa",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./icon-256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./maskable_icon_x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "./icon-1024.png",
|
||||
"sizes": "1024x1024",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"lang": "en-US",
|
||||
"id": "/index.html",
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "white",
|
||||
"theme_color": "white"
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
var cacheName = 'egui-template-pwa';
|
||||
var filesToCache = [
|
||||
'./',
|
||||
'./index.html',
|
||||
'./eframe_template.js',
|
||||
'./eframe_template_bg.wasm',
|
||||
];
|
||||
|
||||
/* Start the service worker and cache all of the app's content */
|
||||
self.addEventListener('install', function (e) {
|
||||
e.waitUntil(
|
||||
caches.open(cacheName).then(function (cache) {
|
||||
return cache.addAll(filesToCache);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/* Serve cached content when offline */
|
||||
self.addEventListener('fetch', function (e) {
|
||||
e.respondWith(
|
||||
caches.match(e.request).then(function (response) {
|
||||
return response || fetch(e.request);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -1,245 +1,23 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
use enostr::FullKeypair;
|
||||
use nostrdb::Ndb;
|
||||
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use enostr::{ClientMessage, FilledKeypair, FullKeypair, Keypair, RelayPool};
|
||||
use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction};
|
||||
use notedeck::{Accounts, AccountsAction, AddAccountAction, ImageCache, SingleUnkIdAction};
|
||||
|
||||
use crate::app::get_active_columns_mut;
|
||||
use crate::decks::DecksCache;
|
||||
use crate::{
|
||||
imgcache::ImageCache,
|
||||
login_manager::AcquireKeyState,
|
||||
muted::Muted,
|
||||
route::Route,
|
||||
storage::{KeyStorageResponse, KeyStorageType},
|
||||
ui::{
|
||||
account_login_view::{AccountLoginResponse, AccountLoginView},
|
||||
accounts::{AccountsView, AccountsViewResponse},
|
||||
},
|
||||
unknowns::SingleUnkIdAction,
|
||||
unknowns::UnknownIds,
|
||||
user_account::UserAccount,
|
||||
};
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::info;
|
||||
|
||||
mod route;
|
||||
|
||||
pub use route::{AccountsAction, AccountsRoute, AccountsRouteResponse};
|
||||
|
||||
pub struct AccountRelayData {
|
||||
filter: Filter,
|
||||
subid: String,
|
||||
sub: Option<Subscription>,
|
||||
local: BTreeSet<String>, // used locally but not advertised
|
||||
advertised: BTreeSet<String>, // advertised via NIP-65
|
||||
}
|
||||
|
||||
impl AccountRelayData {
|
||||
pub fn new(ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) -> Self {
|
||||
// Construct a filter for the user's NIP-65 relay list
|
||||
let filter = Filter::new()
|
||||
.authors([pubkey])
|
||||
.kinds([10002])
|
||||
.limit(1)
|
||||
.build();
|
||||
|
||||
// Local ndb subscription
|
||||
let ndbsub = ndb
|
||||
.subscribe(&[filter.clone()])
|
||||
.expect("ndb relay list subscription");
|
||||
|
||||
// Query the ndb immediately to see if the user list is already there
|
||||
let txn = Transaction::new(ndb).expect("transaction");
|
||||
let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32;
|
||||
let nks = ndb
|
||||
.query(&txn, &[filter.clone()], lim)
|
||||
.expect("query user relays results")
|
||||
.iter()
|
||||
.map(|qr| qr.note_key)
|
||||
.collect::<Vec<NoteKey>>();
|
||||
let relays = Self::harvest_nip65_relays(ndb, &txn, &nks);
|
||||
debug!(
|
||||
"pubkey {}: initial relays {:?}",
|
||||
hex::encode(pubkey),
|
||||
relays
|
||||
);
|
||||
|
||||
// Id for future remote relay subscriptions
|
||||
let subid = Uuid::new_v4().to_string();
|
||||
|
||||
// Add remote subscription to existing relays
|
||||
pool.subscribe(subid.clone(), vec![filter.clone()]);
|
||||
|
||||
AccountRelayData {
|
||||
filter,
|
||||
subid,
|
||||
sub: Some(ndbsub),
|
||||
local: BTreeSet::new(),
|
||||
advertised: relays.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// standardize the format (ie, trailing slashes) to avoid dups
|
||||
pub fn canonicalize_url(url: &str) -> String {
|
||||
match Url::parse(url) {
|
||||
Ok(parsed_url) => parsed_url.to_string(),
|
||||
Err(_) => url.to_owned(), // If parsing fails, return the original URL.
|
||||
}
|
||||
}
|
||||
|
||||
fn harvest_nip65_relays(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Vec<String> {
|
||||
let mut relays = Vec::new();
|
||||
for nk in nks.iter() {
|
||||
if let Ok(note) = ndb.get_note_by_key(txn, *nk) {
|
||||
for tag in note.tags() {
|
||||
match tag.get(0).and_then(|t| t.variant().str()) {
|
||||
Some("r") => {
|
||||
if let Some(url) = tag.get(1).and_then(|f| f.variant().str()) {
|
||||
relays.push(Self::canonicalize_url(url));
|
||||
}
|
||||
}
|
||||
Some("alt") => {
|
||||
// ignore for now
|
||||
}
|
||||
Some(x) => {
|
||||
error!("harvest_nip65_relays: unexpected tag type: {}", x);
|
||||
}
|
||||
None => {
|
||||
error!("harvest_nip65_relays: invalid tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
relays
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AccountMutedData {
|
||||
filter: Filter,
|
||||
subid: String,
|
||||
sub: Option<Subscription>,
|
||||
muted: Arc<Muted>,
|
||||
}
|
||||
|
||||
impl AccountMutedData {
|
||||
pub fn new(ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) -> Self {
|
||||
// Construct a filter for the user's NIP-51 muted list
|
||||
let filter = Filter::new()
|
||||
.authors([pubkey])
|
||||
.kinds([10000])
|
||||
.limit(1)
|
||||
.build();
|
||||
|
||||
// Local ndb subscription
|
||||
let ndbsub = ndb
|
||||
.subscribe(&[filter.clone()])
|
||||
.expect("ndb muted subscription");
|
||||
|
||||
// Query the ndb immediately to see if the user's muted list is already there
|
||||
let txn = Transaction::new(ndb).expect("transaction");
|
||||
let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32;
|
||||
let nks = ndb
|
||||
.query(&txn, &[filter.clone()], lim)
|
||||
.expect("query user muted results")
|
||||
.iter()
|
||||
.map(|qr| qr.note_key)
|
||||
.collect::<Vec<NoteKey>>();
|
||||
let muted = Self::harvest_nip51_muted(ndb, &txn, &nks);
|
||||
debug!("pubkey {}: initial muted {:?}", hex::encode(pubkey), muted);
|
||||
|
||||
// Id for future remote relay subscriptions
|
||||
let subid = Uuid::new_v4().to_string();
|
||||
|
||||
// Add remote subscription to existing relays
|
||||
pool.subscribe(subid.clone(), vec![filter.clone()]);
|
||||
|
||||
AccountMutedData {
|
||||
filter,
|
||||
subid,
|
||||
sub: Some(ndbsub),
|
||||
muted: Arc::new(muted),
|
||||
}
|
||||
}
|
||||
|
||||
fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted {
|
||||
let mut muted = Muted::default();
|
||||
for nk in nks.iter() {
|
||||
if let Ok(note) = ndb.get_note_by_key(txn, *nk) {
|
||||
for tag in note.tags() {
|
||||
match tag.get(0).and_then(|t| t.variant().str()) {
|
||||
Some("p") => {
|
||||
if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) {
|
||||
muted.pubkeys.insert(*id);
|
||||
}
|
||||
}
|
||||
Some("t") => {
|
||||
if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) {
|
||||
muted.hashtags.insert(str.to_string());
|
||||
}
|
||||
}
|
||||
Some("word") => {
|
||||
if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) {
|
||||
muted.words.insert(str.to_string());
|
||||
}
|
||||
}
|
||||
Some("e") => {
|
||||
if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) {
|
||||
muted.threads.insert(*id);
|
||||
}
|
||||
}
|
||||
Some("alt") => {
|
||||
// maybe we can ignore these?
|
||||
}
|
||||
Some(x) => error!("query_nip51_muted: unexpected tag: {}", x),
|
||||
None => error!(
|
||||
"query_nip51_muted: bad tag value: {:?}",
|
||||
tag.get_unchecked(0).variant()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
muted
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AccountData {
|
||||
relay: AccountRelayData,
|
||||
muted: AccountMutedData,
|
||||
}
|
||||
|
||||
/// The interface for managing the user's accounts.
|
||||
/// Represents all user-facing operations related to account management.
|
||||
pub struct Accounts {
|
||||
currently_selected_account: Option<usize>,
|
||||
accounts: Vec<UserAccount>,
|
||||
key_store: KeyStorageType,
|
||||
account_data: BTreeMap<[u8; 32], AccountData>,
|
||||
forced_relays: BTreeSet<String>,
|
||||
bootstrap_relays: BTreeSet<String>,
|
||||
needs_relay_config: bool,
|
||||
}
|
||||
|
||||
#[must_use = "You must call process_login_action on this to handle unknown ids"]
|
||||
pub struct RenderAccountAction {
|
||||
pub accounts_action: Option<AccountsAction>,
|
||||
pub unk_id_action: SingleUnkIdAction,
|
||||
}
|
||||
|
||||
impl RenderAccountAction {
|
||||
// Simple wrapper around processing the unknown action to expose too
|
||||
// much internal logic. This allows us to have a must_use on our
|
||||
// LoginAction type, otherwise the SingleUnkIdAction's must_use will
|
||||
// be lost when returned in the login action
|
||||
pub fn process_action(&mut self, ids: &mut UnknownIds, ndb: &Ndb, txn: &Transaction) {
|
||||
self.unk_id_action.process_action(ids, ndb, txn);
|
||||
}
|
||||
}
|
||||
pub use route::{AccountsRoute, AccountsRouteResponse};
|
||||
|
||||
/// Render account management views from a route
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -252,7 +30,7 @@ pub fn render_accounts_route(
|
||||
decks: &mut DecksCache,
|
||||
login_state: &mut AcquireKeyState,
|
||||
route: AccountsRoute,
|
||||
) -> RenderAccountAction {
|
||||
) -> AddAccountAction {
|
||||
let resp = match route {
|
||||
AccountsRoute::Accounts => AccountsView::new(ndb, accounts, img_cache)
|
||||
.ui(ui)
|
||||
@@ -269,7 +47,7 @@ pub fn render_accounts_route(
|
||||
match resp {
|
||||
AccountsRouteResponse::Accounts(response) => {
|
||||
let action = process_accounts_view_response(accounts, decks, col, response);
|
||||
RenderAccountAction {
|
||||
AddAccountAction {
|
||||
accounts_action: action,
|
||||
unk_id_action: SingleUnkIdAction::no_action(),
|
||||
}
|
||||
@@ -285,7 +63,7 @@ pub fn render_accounts_route(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
RenderAccountAction {
|
||||
AddAccountAction {
|
||||
accounts_action: None,
|
||||
unk_id_action: SingleUnkIdAction::no_action(),
|
||||
}
|
||||
@@ -321,343 +99,11 @@ pub fn process_accounts_view_response(
|
||||
selection
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
pub fn new(key_store: KeyStorageType, forced_relays: Vec<String>) -> Self {
|
||||
let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() {
|
||||
res.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let currently_selected_account = get_selected_index(&accounts, &key_store);
|
||||
let account_data = BTreeMap::new();
|
||||
let forced_relays: BTreeSet<String> = forced_relays
|
||||
.into_iter()
|
||||
.map(|u| AccountRelayData::canonicalize_url(&u))
|
||||
.collect();
|
||||
let bootstrap_relays = [
|
||||
"wss://relay.damus.io",
|
||||
// "wss://pyramid.fiatjaf.com", // Uncomment if needed
|
||||
"wss://nos.lol",
|
||||
"wss://nostr.wine",
|
||||
"wss://purplepag.es",
|
||||
]
|
||||
.iter()
|
||||
.map(|&url| url.to_string())
|
||||
.map(|u| AccountRelayData::canonicalize_url(&u))
|
||||
.collect();
|
||||
|
||||
Accounts {
|
||||
currently_selected_account,
|
||||
accounts,
|
||||
key_store,
|
||||
account_data,
|
||||
forced_relays,
|
||||
bootstrap_relays,
|
||||
needs_relay_config: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_accounts(&self) -> &Vec<UserAccount> {
|
||||
&self.accounts
|
||||
}
|
||||
|
||||
pub fn get_account(&self, ind: usize) -> Option<&UserAccount> {
|
||||
self.accounts.get(ind)
|
||||
}
|
||||
|
||||
pub fn find_account(&self, pk: &[u8; 32]) -> Option<&UserAccount> {
|
||||
self.accounts.iter().find(|acc| acc.pubkey.bytes() == pk)
|
||||
}
|
||||
|
||||
pub fn remove_account(&mut self, index: usize) {
|
||||
if let Some(account) = self.accounts.get(index) {
|
||||
let _ = self.key_store.remove_key(account);
|
||||
self.accounts.remove(index);
|
||||
|
||||
if let Some(selected_index) = self.currently_selected_account {
|
||||
match selected_index.cmp(&index) {
|
||||
Ordering::Greater => {
|
||||
self.select_account(selected_index - 1);
|
||||
}
|
||||
Ordering::Equal => {
|
||||
if self.accounts.is_empty() {
|
||||
// If no accounts remain, clear the selection
|
||||
self.clear_selected_account();
|
||||
} else if index >= self.accounts.len() {
|
||||
// If the removed account was the last one, select the new last account
|
||||
self.select_account(self.accounts.len() - 1);
|
||||
} else {
|
||||
// Otherwise, select the account at the same position
|
||||
self.select_account(index);
|
||||
}
|
||||
}
|
||||
Ordering::Less => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_account(&self, pubkey: &[u8; 32]) -> Option<ContainsAccount> {
|
||||
for (index, account) in self.accounts.iter().enumerate() {
|
||||
let has_pubkey = account.pubkey.bytes() == pubkey;
|
||||
let has_nsec = account.secret_key.is_some();
|
||||
if has_pubkey {
|
||||
return Some(ContainsAccount { has_nsec, index });
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"]
|
||||
pub fn add_account(&mut self, account: Keypair) -> RenderAccountAction {
|
||||
let pubkey = account.pubkey;
|
||||
let switch_to_index = if let Some(contains_acc) = self.contains_account(pubkey.bytes()) {
|
||||
if account.secret_key.is_some() && !contains_acc.has_nsec {
|
||||
info!(
|
||||
"user provided nsec, but we already have npub {}. Upgrading to nsec",
|
||||
pubkey
|
||||
);
|
||||
let _ = self.key_store.add_key(&account);
|
||||
|
||||
self.accounts[contains_acc.index] = account;
|
||||
} else {
|
||||
info!("already have account, not adding {}", pubkey);
|
||||
}
|
||||
contains_acc.index
|
||||
} else {
|
||||
info!("adding new account {}", pubkey);
|
||||
let _ = self.key_store.add_key(&account);
|
||||
self.accounts.push(account);
|
||||
self.accounts.len() - 1
|
||||
};
|
||||
|
||||
RenderAccountAction {
|
||||
accounts_action: Some(AccountsAction::Switch(switch_to_index)),
|
||||
unk_id_action: SingleUnkIdAction::pubkey(pubkey),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_accounts(&self) -> usize {
|
||||
self.accounts.len()
|
||||
}
|
||||
|
||||
pub fn get_selected_account_index(&self) -> Option<usize> {
|
||||
self.currently_selected_account
|
||||
}
|
||||
|
||||
pub fn selected_or_first_nsec(&self) -> Option<FilledKeypair<'_>> {
|
||||
self.get_selected_account()
|
||||
.and_then(|kp| kp.to_full())
|
||||
.or_else(|| self.accounts.iter().find_map(|a| a.to_full()))
|
||||
}
|
||||
|
||||
pub fn get_selected_account(&self) -> Option<&UserAccount> {
|
||||
if let Some(account_index) = self.currently_selected_account {
|
||||
if let Some(account) = self.get_account(account_index) {
|
||||
Some(account)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_account(&mut self, index: usize) {
|
||||
if let Some(account) = self.accounts.get(index) {
|
||||
self.currently_selected_account = Some(index);
|
||||
self.key_store.select_key(Some(account.pubkey));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_selected_account(&mut self) {
|
||||
self.currently_selected_account = None;
|
||||
self.key_store.select_key(None);
|
||||
}
|
||||
|
||||
pub fn mutefun(&self) -> Box<dyn Fn(&Note) -> bool> {
|
||||
if let Some(index) = self.currently_selected_account {
|
||||
if let Some(account) = self.accounts.get(index) {
|
||||
let pubkey = account.pubkey.bytes();
|
||||
if let Some(account_data) = self.account_data.get(pubkey) {
|
||||
let muted = Arc::clone(&account_data.muted.muted);
|
||||
return Box::new(move |note: &Note| muted.is_muted(note));
|
||||
}
|
||||
}
|
||||
}
|
||||
Box::new(|_: &Note| false)
|
||||
}
|
||||
|
||||
pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) {
|
||||
for data in self.account_data.values() {
|
||||
pool.send_to(
|
||||
&ClientMessage::req(data.relay.subid.clone(), vec![data.relay.filter.clone()]),
|
||||
relay_url,
|
||||
);
|
||||
pool.send_to(
|
||||
&ClientMessage::req(data.muted.subid.clone(), vec![data.muted.filter.clone()]),
|
||||
relay_url,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns added and removed accounts
|
||||
fn delta_accounts(&self) -> (Vec<[u8; 32]>, Vec<[u8; 32]>) {
|
||||
let mut added = Vec::new();
|
||||
for pubkey in self.accounts.iter().map(|a| a.pubkey.bytes()) {
|
||||
if !self.account_data.contains_key(pubkey) {
|
||||
added.push(*pubkey);
|
||||
}
|
||||
}
|
||||
let mut removed = Vec::new();
|
||||
for pubkey in self.account_data.keys() {
|
||||
if self.contains_account(pubkey).is_none() {
|
||||
removed.push(*pubkey);
|
||||
}
|
||||
}
|
||||
(added, removed)
|
||||
}
|
||||
|
||||
fn handle_added_account(&mut self, ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) {
|
||||
debug!("handle_added_account {}", hex::encode(pubkey));
|
||||
|
||||
// Create the user account data
|
||||
let new_account_data = AccountData {
|
||||
relay: AccountRelayData::new(ndb, pool, pubkey),
|
||||
muted: AccountMutedData::new(ndb, pool, pubkey),
|
||||
};
|
||||
self.account_data.insert(*pubkey, new_account_data);
|
||||
}
|
||||
|
||||
fn handle_removed_account(&mut self, pubkey: &[u8; 32]) {
|
||||
debug!("handle_removed_account {}", hex::encode(pubkey));
|
||||
// FIXME - we need to unsubscribe here
|
||||
self.account_data.remove(pubkey);
|
||||
}
|
||||
|
||||
fn poll_for_updates(&mut self, ndb: &Ndb) -> bool {
|
||||
let mut changed = false;
|
||||
for (pubkey, data) in &mut self.account_data {
|
||||
if let Some(sub) = data.relay.sub {
|
||||
let nks = ndb.poll_for_notes(sub, 1);
|
||||
if !nks.is_empty() {
|
||||
let txn = Transaction::new(ndb).expect("txn");
|
||||
let relays = AccountRelayData::harvest_nip65_relays(ndb, &txn, &nks);
|
||||
debug!(
|
||||
"pubkey {}: updated relays {:?}",
|
||||
hex::encode(pubkey),
|
||||
relays
|
||||
);
|
||||
data.relay.advertised = relays.into_iter().collect();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if let Some(sub) = data.muted.sub {
|
||||
let nks = ndb.poll_for_notes(sub, 1);
|
||||
if !nks.is_empty() {
|
||||
let txn = Transaction::new(ndb).expect("txn");
|
||||
let muted = AccountMutedData::harvest_nip51_muted(ndb, &txn, &nks);
|
||||
debug!("pubkey {}: updated muted {:?}", hex::encode(pubkey), muted);
|
||||
data.muted.muted = Arc::new(muted);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn update_relay_configuration(
|
||||
&mut self,
|
||||
pool: &mut RelayPool,
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) {
|
||||
// If forced relays are set use them only
|
||||
let mut desired_relays = self.forced_relays.clone();
|
||||
|
||||
// Compose the desired relay lists from the accounts
|
||||
if desired_relays.is_empty() {
|
||||
for data in self.account_data.values() {
|
||||
desired_relays.extend(data.relay.local.iter().cloned());
|
||||
desired_relays.extend(data.relay.advertised.iter().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
// If no relays are specified at this point use the bootstrap list
|
||||
if desired_relays.is_empty() {
|
||||
desired_relays = self.bootstrap_relays.clone();
|
||||
}
|
||||
|
||||
debug!("current relays: {:?}", pool.urls());
|
||||
debug!("desired relays: {:?}", desired_relays);
|
||||
|
||||
let add: BTreeSet<String> = desired_relays.difference(&pool.urls()).cloned().collect();
|
||||
let sub: BTreeSet<String> = pool.urls().difference(&desired_relays).cloned().collect();
|
||||
if !add.is_empty() {
|
||||
debug!("configuring added relays: {:?}", add);
|
||||
let _ = pool.add_urls(add, wakeup);
|
||||
}
|
||||
if !sub.is_empty() {
|
||||
debug!("removing unwanted relays: {:?}", sub);
|
||||
pool.remove_urls(&sub);
|
||||
}
|
||||
|
||||
debug!("current relays: {:?}", pool.urls());
|
||||
}
|
||||
|
||||
pub fn update(&mut self, ndb: &Ndb, pool: &mut RelayPool, ctx: &egui::Context) {
|
||||
// IMPORTANT - This function is called in the UI update loop,
|
||||
// make sure it is fast when idle
|
||||
|
||||
// On the initial update the relays need config even if nothing changes below
|
||||
let mut relays_changed = self.needs_relay_config;
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let wakeup = move || {
|
||||
ctx2.request_repaint();
|
||||
};
|
||||
|
||||
// Were any accounts added or removed?
|
||||
let (added, removed) = self.delta_accounts();
|
||||
for pk in added {
|
||||
self.handle_added_account(ndb, pool, &pk);
|
||||
relays_changed = true;
|
||||
}
|
||||
for pk in removed {
|
||||
self.handle_removed_account(&pk);
|
||||
relays_changed = true;
|
||||
}
|
||||
|
||||
// Did any accounts receive updates (ie NIP-65 relay lists)
|
||||
relays_changed = self.poll_for_updates(ndb) || relays_changed;
|
||||
|
||||
// If needed, update the relay configuration
|
||||
if relays_changed {
|
||||
self.update_relay_configuration(pool, wakeup);
|
||||
self.needs_relay_config = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option<usize> {
|
||||
match keystore.get_selected_key() {
|
||||
KeyStorageResponse::ReceivedResult(Ok(Some(pubkey))) => {
|
||||
return accounts.iter().position(|account| account.pubkey == pubkey);
|
||||
}
|
||||
|
||||
KeyStorageResponse::ReceivedResult(Err(e)) => error!("Error getting selected key: {}", e),
|
||||
KeyStorageResponse::Waiting | KeyStorageResponse::ReceivedResult(Ok(None)) => {}
|
||||
};
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn process_login_view_response(
|
||||
manager: &mut Accounts,
|
||||
decks: &mut DecksCache,
|
||||
response: AccountLoginResponse,
|
||||
) -> RenderAccountAction {
|
||||
) -> AddAccountAction {
|
||||
let (r, pubkey) = match response {
|
||||
AccountLoginResponse::CreateNew => {
|
||||
let kp = FullKeypair::generate().to_keypair();
|
||||
@@ -674,9 +120,3 @@ pub fn process_login_view_response(
|
||||
|
||||
r
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ContainsAccount {
|
||||
pub has_nsec: bool,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
@@ -11,9 +11,3 @@ pub enum AccountsRoute {
|
||||
Accounts,
|
||||
AddAccount,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AccountsAction {
|
||||
Switch(usize),
|
||||
Remove(usize),
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use crate::{
|
||||
column::Columns,
|
||||
muted::MuteFun,
|
||||
note::NoteRef,
|
||||
notecache::NoteCache,
|
||||
notes_holder::{NotesHolder, NotesHolderStorage},
|
||||
profile::Profile,
|
||||
route::{Route, Router},
|
||||
thread::Thread,
|
||||
};
|
||||
|
||||
use enostr::{NoteId, Pubkey, RelayPool};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{note::root_note_id_from_selected_id, MuteFun, NoteCache, NoteRef};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum NoteAction {
|
||||
@@ -46,7 +45,7 @@ fn open_thread(
|
||||
) -> Option<NotesHolderResult> {
|
||||
router.route_to(Route::thread(NoteId::new(selected_note.to_owned())));
|
||||
|
||||
let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, txn, selected_note);
|
||||
let root_id = root_note_id_from_selected_id(ndb, note_cache, txn, selected_note);
|
||||
Thread::open(ndb, note_cache, txn, pool, threads, root_id, is_muted)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,31 @@
|
||||
use crate::{
|
||||
accounts::Accounts,
|
||||
app_creation::setup_cc,
|
||||
app_size_handler::AppSizeHandler,
|
||||
app_style::{dark_mode, light_mode},
|
||||
args::Args,
|
||||
args::ColumnsArgs,
|
||||
column::Columns,
|
||||
decks::{Decks, DecksCache, FALLBACK_PUBKEY},
|
||||
draft::Drafts,
|
||||
filter::FilterState,
|
||||
frame_history::FrameHistory,
|
||||
imgcache::ImageCache,
|
||||
nav,
|
||||
notecache::NoteCache,
|
||||
notes_holder::NotesHolderStorage,
|
||||
profile::Profile,
|
||||
storage::{self, DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageType},
|
||||
storage,
|
||||
subscriptions::{SubKind, Subscriptions},
|
||||
support::Support,
|
||||
theme_handler::ThemeHandler,
|
||||
thread::Thread,
|
||||
timeline::{self, Timeline},
|
||||
ui::{self, is_compiled_as_mobile, DesktopSidePanel},
|
||||
unknowns::UnknownIds,
|
||||
ui::{self, DesktopSidePanel},
|
||||
unknowns,
|
||||
view_state::ViewState,
|
||||
Result,
|
||||
};
|
||||
|
||||
use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, ImageCache, UnknownIds};
|
||||
|
||||
use enostr::{ClientMessage, Keypair, Pubkey, RelayEvent, RelayMessage, RelayPool};
|
||||
use uuid::Uuid;
|
||||
|
||||
use egui::{Context, Frame, Style};
|
||||
use egui::{Frame, Style};
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
|
||||
use nostrdb::{Config, Ndb, Transaction};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
@@ -48,26 +41,16 @@ pub enum DamusState {
|
||||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||
pub struct Damus {
|
||||
state: DamusState,
|
||||
pub note_cache: NoteCache,
|
||||
pub pool: RelayPool,
|
||||
|
||||
pub decks_cache: DecksCache,
|
||||
pub ndb: Ndb,
|
||||
pub view_state: ViewState,
|
||||
pub unknown_ids: UnknownIds,
|
||||
pub drafts: Drafts,
|
||||
pub threads: NotesHolderStorage<Thread>,
|
||||
pub profiles: NotesHolderStorage<Profile>,
|
||||
pub img_cache: ImageCache,
|
||||
pub accounts: Accounts,
|
||||
pub subscriptions: Subscriptions,
|
||||
pub app_rect_handler: AppSizeHandler,
|
||||
pub support: Support,
|
||||
pub theme: ThemeHandler,
|
||||
|
||||
frame_history: crate::frame_history::FrameHistory,
|
||||
//frame_history: crate::frame_history::FrameHistory,
|
||||
|
||||
pub path: DataPath,
|
||||
// TODO: make these bitflags
|
||||
pub debug: bool,
|
||||
pub since_optimize: bool,
|
||||
@@ -99,21 +82,26 @@ fn handle_key_events(input: &egui::InputState, _pixels_per_point: f32, columns:
|
||||
}
|
||||
}
|
||||
|
||||
fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> {
|
||||
fn try_process_event(
|
||||
damus: &mut Damus,
|
||||
app_ctx: &mut AppContext<'_>,
|
||||
ctx: &egui::Context,
|
||||
) -> Result<()> {
|
||||
let ppp = ctx.pixels_per_point();
|
||||
let current_columns = get_active_columns_mut(&damus.accounts, &mut damus.decks_cache);
|
||||
let current_columns = get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache);
|
||||
ctx.input(|i| handle_key_events(i, ppp, current_columns));
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let wakeup = move || {
|
||||
ctx2.request_repaint();
|
||||
};
|
||||
damus.pool.keepalive_ping(wakeup);
|
||||
|
||||
app_ctx.pool.keepalive_ping(wakeup);
|
||||
|
||||
// NOTE: we don't use the while let loop due to borrow issues
|
||||
#[allow(clippy::while_let_loop)]
|
||||
loop {
|
||||
let ev = if let Some(ev) = damus.pool.try_recv() {
|
||||
let ev = if let Some(ev) = app_ctx.pool.try_recv() {
|
||||
ev.into_owned()
|
||||
} else {
|
||||
break;
|
||||
@@ -121,16 +109,16 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> {
|
||||
|
||||
match (&ev.event).into() {
|
||||
RelayEvent::Opened => {
|
||||
damus
|
||||
app_ctx
|
||||
.accounts
|
||||
.send_initial_filters(&mut damus.pool, &ev.relay);
|
||||
.send_initial_filters(app_ctx.pool, &ev.relay);
|
||||
|
||||
timeline::send_initial_timeline_filters(
|
||||
&damus.ndb,
|
||||
app_ctx.ndb,
|
||||
damus.since_optimize,
|
||||
get_active_columns_mut(&damus.accounts, &mut damus.decks_cache),
|
||||
get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache),
|
||||
&mut damus.subscriptions,
|
||||
&mut damus.pool,
|
||||
app_ctx.pool,
|
||||
&ev.relay,
|
||||
);
|
||||
}
|
||||
@@ -138,35 +126,35 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> {
|
||||
RelayEvent::Closed => warn!("{} connection closed", &ev.relay),
|
||||
RelayEvent::Error(e) => error!("{}: {}", &ev.relay, e),
|
||||
RelayEvent::Other(msg) => trace!("other event {:?}", &msg),
|
||||
RelayEvent::Message(msg) => process_message(damus, &ev.relay, &msg),
|
||||
RelayEvent::Message(msg) => process_message(damus, app_ctx, &ev.relay, &msg),
|
||||
}
|
||||
}
|
||||
|
||||
let current_columns = get_active_columns_mut(&damus.accounts, &mut damus.decks_cache);
|
||||
let current_columns = get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache);
|
||||
let n_timelines = current_columns.timelines().len();
|
||||
for timeline_ind in 0..n_timelines {
|
||||
let is_ready = {
|
||||
let timeline = &mut current_columns.timelines[timeline_ind];
|
||||
timeline::is_timeline_ready(
|
||||
&damus.ndb,
|
||||
&mut damus.pool,
|
||||
&mut damus.note_cache,
|
||||
app_ctx.ndb,
|
||||
app_ctx.pool,
|
||||
app_ctx.note_cache,
|
||||
timeline,
|
||||
&damus.accounts.mutefun(),
|
||||
&app_ctx.accounts.mutefun(),
|
||||
)
|
||||
};
|
||||
|
||||
if is_ready {
|
||||
let txn = Transaction::new(&damus.ndb).expect("txn");
|
||||
let txn = Transaction::new(app_ctx.ndb).expect("txn");
|
||||
|
||||
if let Err(err) = Timeline::poll_notes_into_view(
|
||||
timeline_ind,
|
||||
current_columns.timelines_mut(),
|
||||
&damus.ndb,
|
||||
app_ctx.ndb,
|
||||
&txn,
|
||||
&mut damus.unknown_ids,
|
||||
&mut damus.note_cache,
|
||||
&damus.accounts.mutefun(),
|
||||
app_ctx.unknown_ids,
|
||||
app_ctx.note_cache,
|
||||
&app_ctx.accounts.mutefun(),
|
||||
) {
|
||||
error!("poll_notes_into_view: {err}");
|
||||
}
|
||||
@@ -175,22 +163,22 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
if damus.unknown_ids.ready_to_send() {
|
||||
unknown_id_send(damus);
|
||||
if app_ctx.unknown_ids.ready_to_send() {
|
||||
unknown_id_send(app_ctx.unknown_ids, app_ctx.pool);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unknown_id_send(damus: &mut Damus) {
|
||||
let filter = damus.unknown_ids.filter().expect("filter");
|
||||
fn unknown_id_send(unknown_ids: &mut UnknownIds, pool: &mut RelayPool) {
|
||||
let filter = unknown_ids.filter().expect("filter");
|
||||
info!(
|
||||
"Getting {} unknown ids from relays",
|
||||
damus.unknown_ids.ids().len()
|
||||
unknown_ids.ids().len()
|
||||
);
|
||||
let msg = ClientMessage::req("unknownids".to_string(), filter);
|
||||
damus.unknown_ids.clear();
|
||||
damus.pool.send(&msg);
|
||||
unknown_ids.clear();
|
||||
pool.send(&msg);
|
||||
}
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
@@ -198,8 +186,11 @@ fn setup_profiling() {
|
||||
puffin::set_scopes_on(true); // tell puffin to collect data
|
||||
}
|
||||
|
||||
fn update_damus(damus: &mut Damus, ctx: &egui::Context) {
|
||||
damus.accounts.update(&damus.ndb, &mut damus.pool, ctx); // update user relay and mute lists
|
||||
fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>) {
|
||||
let _ctx = app_ctx.egui.clone();
|
||||
let ctx = &_ctx;
|
||||
|
||||
app_ctx.accounts.update(app_ctx.ndb, app_ctx.pool, ctx); // update user relay and mute lists
|
||||
|
||||
match damus.state {
|
||||
DamusState::Initializing => {
|
||||
@@ -212,10 +203,10 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) {
|
||||
.subscriptions()
|
||||
.insert("unknownids".to_string(), SubKind::OneShot);
|
||||
if let Err(err) = timeline::setup_initial_nostrdb_subs(
|
||||
&damus.ndb,
|
||||
&mut damus.note_cache,
|
||||
app_ctx.ndb,
|
||||
app_ctx.note_cache,
|
||||
&mut damus.decks_cache,
|
||||
&damus.accounts.mutefun(),
|
||||
&app_ctx.accounts.mutefun(),
|
||||
) {
|
||||
warn!("update_damus init: {err}");
|
||||
}
|
||||
@@ -224,24 +215,27 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) {
|
||||
DamusState::Initialized => (),
|
||||
};
|
||||
|
||||
if let Err(err) = try_process_event(damus, ctx) {
|
||||
if let Err(err) = try_process_event(damus, app_ctx, ctx) {
|
||||
error!("error processing event: {}", err);
|
||||
}
|
||||
|
||||
damus.app_rect_handler.try_save_app_size(ctx);
|
||||
}
|
||||
|
||||
fn process_event(damus: &mut Damus, _subid: &str, event: &str) {
|
||||
fn process_event(ndb: &Ndb, _subid: &str, event: &str) {
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_function!();
|
||||
|
||||
//info!("processing event {}", event);
|
||||
if let Err(_err) = damus.ndb.process_event(event) {
|
||||
if let Err(_err) = ndb.process_event(event) {
|
||||
error!("error processing event {}", event);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> {
|
||||
fn handle_eose(
|
||||
damus: &mut Damus,
|
||||
ctx: &mut AppContext<'_>,
|
||||
subid: &str,
|
||||
relay_url: &str,
|
||||
) -> Result<()> {
|
||||
let sub_kind = if let Some(sub_kind) = damus.subscriptions().get(subid) {
|
||||
sub_kind
|
||||
} else {
|
||||
@@ -258,29 +252,29 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> {
|
||||
// eose on timeline? whatevs
|
||||
}
|
||||
SubKind::Initial => {
|
||||
let txn = Transaction::new(&damus.ndb)?;
|
||||
UnknownIds::update(
|
||||
let txn = Transaction::new(ctx.ndb)?;
|
||||
unknowns::update_from_columns(
|
||||
&txn,
|
||||
&mut damus.unknown_ids,
|
||||
get_active_columns(&damus.accounts, &damus.decks_cache),
|
||||
&damus.ndb,
|
||||
&mut damus.note_cache,
|
||||
ctx.unknown_ids,
|
||||
get_active_columns(ctx.accounts, &damus.decks_cache),
|
||||
ctx.ndb,
|
||||
ctx.note_cache,
|
||||
);
|
||||
// this is possible if this is the first time
|
||||
if damus.unknown_ids.ready_to_send() {
|
||||
unknown_id_send(damus);
|
||||
if ctx.unknown_ids.ready_to_send() {
|
||||
unknown_id_send(ctx.unknown_ids, ctx.pool);
|
||||
}
|
||||
}
|
||||
|
||||
// oneshot subs just close when they're done
|
||||
SubKind::OneShot => {
|
||||
let msg = ClientMessage::close(subid.to_string());
|
||||
damus.pool.send_to(&msg, relay_url);
|
||||
ctx.pool.send_to(&msg, relay_url);
|
||||
}
|
||||
|
||||
SubKind::FetchingContactList(timeline_uid) => {
|
||||
let timeline = if let Some(tl) =
|
||||
get_active_columns_mut(&damus.accounts, &mut damus.decks_cache)
|
||||
get_active_columns_mut(ctx.accounts, &mut damus.decks_cache)
|
||||
.find_timeline_mut(timeline_uid)
|
||||
{
|
||||
tl
|
||||
@@ -326,27 +320,28 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_message(damus: &mut Damus, relay: &str, msg: &RelayMessage) {
|
||||
fn process_message(damus: &mut Damus, ctx: &mut AppContext<'_>, relay: &str, msg: &RelayMessage) {
|
||||
match msg {
|
||||
RelayMessage::Event(subid, ev) => process_event(damus, subid, ev),
|
||||
RelayMessage::Event(subid, ev) => process_event(ctx.ndb, subid, ev),
|
||||
RelayMessage::Notice(msg) => warn!("Notice from {}: {}", relay, msg),
|
||||
RelayMessage::OK(cr) => info!("OK {:?}", cr),
|
||||
RelayMessage::Eose(sid) => {
|
||||
if let Err(err) = handle_eose(damus, sid, relay) {
|
||||
if let Err(err) = handle_eose(damus, ctx, sid, relay) {
|
||||
error!("error handling eose: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_damus(damus: &mut Damus, ctx: &Context) {
|
||||
if ui::is_narrow(ctx) {
|
||||
render_damus_mobile(ctx, damus);
|
||||
fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>) {
|
||||
if notedeck::ui::is_narrow(app_ctx.egui) {
|
||||
render_damus_mobile(damus, app_ctx);
|
||||
} else {
|
||||
render_damus_desktop(ctx, damus);
|
||||
render_damus_desktop(damus, app_ctx);
|
||||
}
|
||||
|
||||
ctx.request_repaint_after(Duration::from_secs(1));
|
||||
// We use this for keeping timestamps and things up to date
|
||||
app_ctx.egui.request_repaint_after(Duration::from_secs(1));
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin_egui::profiler_window(ctx);
|
||||
@@ -373,91 +368,12 @@ fn determine_key_storage_type() -> KeyStorageType {
|
||||
|
||||
impl Damus {
|
||||
/// Called once before the first frame.
|
||||
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: Vec<String>) -> Self {
|
||||
pub fn new(ctx: &mut AppContext<'_>, args: &[String]) -> Self {
|
||||
// arg parsing
|
||||
let parsed_args = Args::parse(&args);
|
||||
let is_mobile = parsed_args.is_mobile.unwrap_or(ui::is_compiled_as_mobile());
|
||||
|
||||
// Some people have been running notedeck in debug, let's catch that!
|
||||
if !cfg!(test) && cfg!(debug_assertions) && !parsed_args.debug {
|
||||
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
|
||||
println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want.");
|
||||
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
|
||||
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
|
||||
println!("---------------------------------");
|
||||
panic!();
|
||||
}
|
||||
|
||||
setup_cc(ctx, is_mobile, parsed_args.light);
|
||||
|
||||
let data_path = parsed_args
|
||||
.datapath
|
||||
.unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string());
|
||||
let path = DataPath::new(&data_path);
|
||||
let dbpath_str = parsed_args
|
||||
.dbpath
|
||||
.unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string());
|
||||
|
||||
let _ = std::fs::create_dir_all(&dbpath_str);
|
||||
|
||||
let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
|
||||
let _ = std::fs::create_dir_all(imgcache_dir.clone());
|
||||
|
||||
let mapsize = if cfg!(target_os = "windows") {
|
||||
// 16 Gib on windows because it actually creates the file
|
||||
1024usize * 1024usize * 1024usize * 16usize
|
||||
} else {
|
||||
// 1 TiB for everything else since its just virtually mapped
|
||||
1024usize * 1024usize * 1024usize * 1024usize
|
||||
};
|
||||
|
||||
let theme = ThemeHandler::new(&path);
|
||||
ctx.options_mut(|o| {
|
||||
let cur_theme = theme.load();
|
||||
info!("Loaded theme {:?} from disk", cur_theme);
|
||||
o.theme_preference = cur_theme;
|
||||
});
|
||||
ctx.set_visuals_of(egui::Theme::Dark, dark_mode(is_compiled_as_mobile()));
|
||||
ctx.set_visuals_of(egui::Theme::Light, light_mode());
|
||||
|
||||
let config = Config::new().set_ingester_threads(4).set_mapsize(mapsize);
|
||||
|
||||
let keystore = if parsed_args.use_keystore {
|
||||
let keys_path = path.path(DataPathType::Keys);
|
||||
let selected_key_path = path.path(DataPathType::SelectedKey);
|
||||
KeyStorageType::FileSystem(FileKeyStorage::new(
|
||||
Directory::new(keys_path),
|
||||
Directory::new(selected_key_path),
|
||||
))
|
||||
} else {
|
||||
KeyStorageType::None
|
||||
};
|
||||
|
||||
let mut accounts = Accounts::new(keystore, parsed_args.relays);
|
||||
|
||||
let num_keys = parsed_args.keys.len();
|
||||
|
||||
let mut unknown_ids = UnknownIds::default();
|
||||
let ndb = Ndb::new(&dbpath_str, &config).expect("ndb");
|
||||
|
||||
{
|
||||
let txn = Transaction::new(&ndb).expect("txn");
|
||||
for key in parsed_args.keys {
|
||||
info!("adding account: {}", key.pubkey);
|
||||
accounts
|
||||
.add_account(key)
|
||||
.process_action(&mut unknown_ids, &ndb, &txn);
|
||||
}
|
||||
}
|
||||
|
||||
if num_keys != 0 {
|
||||
accounts.select_account(0);
|
||||
}
|
||||
|
||||
// AccountManager will setup the pool on first update
|
||||
let pool = RelayPool::new();
|
||||
|
||||
let account = accounts
|
||||
let parsed_args = ColumnsArgs::parse(args);
|
||||
let account = ctx
|
||||
.accounts
|
||||
.get_selected_account()
|
||||
.as_ref()
|
||||
.map(|a| a.pubkey.bytes());
|
||||
@@ -466,19 +382,19 @@ impl Damus {
|
||||
info!("DecksCache: loading from command line arguments");
|
||||
let mut columns: Columns = Columns::new();
|
||||
for col in parsed_args.columns {
|
||||
if let Some(timeline) = col.into_timeline(&ndb, account) {
|
||||
if let Some(timeline) = col.into_timeline(ctx.ndb, account) {
|
||||
columns.add_new_timeline_column(timeline);
|
||||
}
|
||||
}
|
||||
|
||||
columns_to_decks_cache(columns, account)
|
||||
} else if let Some(decks_cache) = storage::load_decks_cache(&path, &ndb) {
|
||||
} else if let Some(decks_cache) = crate::storage::load_decks_cache(ctx.path, ctx.ndb) {
|
||||
info!(
|
||||
"DecksCache: loading from disk {}",
|
||||
crate::storage::DECKS_CACHE_FILE
|
||||
);
|
||||
decks_cache
|
||||
} else if let Some(cols) = storage::deserialize_columns(&path, &ndb, account) {
|
||||
} else if let Some(cols) = storage::deserialize_columns(ctx.path, ctx.ndb, account) {
|
||||
info!(
|
||||
"DecksCache: loading from disk at depreciated location {}",
|
||||
crate::storage::COLUMNS_FILE
|
||||
@@ -486,79 +402,40 @@ impl Damus {
|
||||
columns_to_decks_cache(cols, account)
|
||||
} else {
|
||||
info!("DecksCache: creating new with demo configuration");
|
||||
let mut cache = DecksCache::new_with_demo_config(&ndb);
|
||||
for account in accounts.get_accounts() {
|
||||
let mut cache = DecksCache::new_with_demo_config(ctx.ndb);
|
||||
for account in ctx.accounts.get_accounts() {
|
||||
cache.add_deck_default(account.pubkey);
|
||||
}
|
||||
set_demo(&mut cache, &ndb, &mut accounts, &mut unknown_ids);
|
||||
set_demo(&mut cache, ctx.ndb, ctx.accounts, ctx.unknown_ids);
|
||||
|
||||
cache
|
||||
};
|
||||
|
||||
let debug = parsed_args.debug;
|
||||
|
||||
let app_rect_handler = AppSizeHandler::new(&path);
|
||||
let support = Support::new(&path);
|
||||
let debug = ctx.args.debug;
|
||||
let support = Support::new(ctx.path);
|
||||
|
||||
Self {
|
||||
pool,
|
||||
debug,
|
||||
unknown_ids,
|
||||
subscriptions: Subscriptions::default(),
|
||||
since_optimize: parsed_args.since_optimize,
|
||||
threads: NotesHolderStorage::default(),
|
||||
profiles: NotesHolderStorage::default(),
|
||||
drafts: Drafts::default(),
|
||||
state: DamusState::Initializing,
|
||||
img_cache: ImageCache::new(imgcache_dir),
|
||||
note_cache: NoteCache::default(),
|
||||
textmode: parsed_args.textmode,
|
||||
ndb,
|
||||
accounts,
|
||||
frame_history: FrameHistory::default(),
|
||||
//frame_history: FrameHistory::default(),
|
||||
view_state: ViewState::default(),
|
||||
path,
|
||||
app_rect_handler,
|
||||
support,
|
||||
decks_cache,
|
||||
theme,
|
||||
debug,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pool_mut(&mut self) -> &mut RelayPool {
|
||||
&mut self.pool
|
||||
pub fn columns_mut(&mut self, accounts: &Accounts) -> &mut Columns {
|
||||
get_active_columns_mut(accounts, &mut self.decks_cache)
|
||||
}
|
||||
|
||||
pub fn ndb(&self) -> &Ndb {
|
||||
&self.ndb
|
||||
}
|
||||
|
||||
pub fn drafts_mut(&mut self) -> &mut Drafts {
|
||||
&mut self.drafts
|
||||
}
|
||||
|
||||
pub fn img_cache_mut(&mut self) -> &mut ImageCache {
|
||||
&mut self.img_cache
|
||||
}
|
||||
|
||||
pub fn accounts(&self) -> &Accounts {
|
||||
&self.accounts
|
||||
}
|
||||
|
||||
pub fn accounts_mut(&mut self) -> &mut Accounts {
|
||||
&mut self.accounts
|
||||
}
|
||||
|
||||
pub fn view_state_mut(&mut self) -> &mut ViewState {
|
||||
&mut self.view_state
|
||||
}
|
||||
|
||||
pub fn columns_mut(&mut self) -> &mut Columns {
|
||||
get_active_columns_mut(&self.accounts, &mut self.decks_cache)
|
||||
}
|
||||
|
||||
pub fn columns(&self) -> &Columns {
|
||||
get_active_columns(&self.accounts, &self.decks_cache)
|
||||
pub fn columns(&self, accounts: &Accounts) -> &Columns {
|
||||
get_active_columns(accounts, &self.decks_cache)
|
||||
}
|
||||
|
||||
pub fn gen_subid(&self, kind: &SubKind) -> String {
|
||||
@@ -573,44 +450,25 @@ impl Damus {
|
||||
let decks_cache = DecksCache::default();
|
||||
|
||||
let path = DataPath::new(&data_path);
|
||||
let theme = ThemeHandler::new(&path);
|
||||
let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
|
||||
let _ = std::fs::create_dir_all(imgcache_dir.clone());
|
||||
let debug = true;
|
||||
|
||||
let app_rect_handler = AppSizeHandler::new(&path);
|
||||
let support = Support::new(&path);
|
||||
|
||||
let config = Config::new().set_ingester_threads(2);
|
||||
|
||||
Self {
|
||||
debug,
|
||||
unknown_ids: UnknownIds::default(),
|
||||
subscriptions: Subscriptions::default(),
|
||||
since_optimize: true,
|
||||
threads: NotesHolderStorage::default(),
|
||||
profiles: NotesHolderStorage::default(),
|
||||
drafts: Drafts::default(),
|
||||
state: DamusState::Initializing,
|
||||
pool: RelayPool::new(),
|
||||
img_cache: ImageCache::new(imgcache_dir),
|
||||
note_cache: NoteCache::default(),
|
||||
textmode: false,
|
||||
ndb: Ndb::new(
|
||||
path.path(DataPathType::Db)
|
||||
.to_str()
|
||||
.expect("db path should be ok"),
|
||||
&config,
|
||||
)
|
||||
.expect("ndb"),
|
||||
accounts: Accounts::new(KeyStorageType::None, vec![]),
|
||||
frame_history: FrameHistory::default(),
|
||||
//frame_history: FrameHistory::default(),
|
||||
view_state: ViewState::default(),
|
||||
path,
|
||||
app_rect_handler,
|
||||
support,
|
||||
decks_cache,
|
||||
theme,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,14 +476,6 @@ impl Damus {
|
||||
&mut self.subscriptions.subs
|
||||
}
|
||||
|
||||
pub fn note_cache_mut(&mut self) -> &mut NoteCache {
|
||||
&mut self.note_cache
|
||||
}
|
||||
|
||||
pub fn unknown_ids_mut(&mut self) -> &mut UnknownIds {
|
||||
&mut self.unknown_ids
|
||||
}
|
||||
|
||||
pub fn threads(&self) -> &NotesHolderStorage<Thread> {
|
||||
&self.threads
|
||||
}
|
||||
@@ -633,10 +483,6 @@ impl Damus {
|
||||
pub fn threads_mut(&mut self) -> &mut NotesHolderStorage<Thread> {
|
||||
&mut self.threads
|
||||
}
|
||||
|
||||
pub fn note_cache(&self) -> &NoteCache {
|
||||
&self.note_cache
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -648,17 +494,20 @@ fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
|
||||
}
|
||||
*/
|
||||
|
||||
fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) {
|
||||
fn render_damus_mobile(app: &mut Damus, app_ctx: &mut AppContext<'_>) {
|
||||
let _ctx = app_ctx.egui.clone();
|
||||
let ctx = &_ctx;
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_function!();
|
||||
|
||||
//let routes = app.timelines[0].routes.clone();
|
||||
|
||||
main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| {
|
||||
if !app.columns().columns().is_empty()
|
||||
&& nav::render_nav(0, app, ui).process_render_nav_response(app)
|
||||
main_panel(&ctx.style(), notedeck::ui::is_narrow(ctx)).show(ctx, |ui| {
|
||||
if !app.columns(app_ctx.accounts).columns().is_empty()
|
||||
&& nav::render_nav(0, app, app_ctx, ui).process_render_nav_response(app, app_ctx)
|
||||
{
|
||||
storage::save_decks_cache(&app.path, &app.decks_cache);
|
||||
storage::save_decks_cache(app_ctx.path, &app.decks_cache);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -677,13 +526,16 @@ fn main_panel(style: &Style, narrow: bool) -> egui::CentralPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
|
||||
fn render_damus_desktop(app: &mut Damus, app_ctx: &mut AppContext<'_>) {
|
||||
let _ctx = app_ctx.egui.clone();
|
||||
let ctx = &_ctx;
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::profile_function!();
|
||||
|
||||
let screen_size = ctx.screen_rect().width();
|
||||
let calc_panel_width = (screen_size
|
||||
/ get_active_columns(&app.accounts, &app.decks_cache).num_columns() as f32)
|
||||
/ get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns() as f32)
|
||||
- 30.0;
|
||||
let min_width = 320.0;
|
||||
let need_scroll = calc_panel_width < min_width;
|
||||
@@ -693,24 +545,24 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
|
||||
Size::remainder()
|
||||
};
|
||||
|
||||
main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| {
|
||||
main_panel(&ctx.style(), notedeck::ui::is_narrow(ctx)).show(ctx, |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
if need_scroll {
|
||||
egui::ScrollArea::horizontal().show(ui, |ui| {
|
||||
timelines_view(ui, panel_sizes, app);
|
||||
timelines_view(ui, panel_sizes, app, app_ctx);
|
||||
});
|
||||
} else {
|
||||
timelines_view(ui, panel_sizes, app);
|
||||
timelines_view(ui, panel_sizes, app, app_ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) {
|
||||
fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut AppContext<'_>) {
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH))
|
||||
.sizes(
|
||||
sizes,
|
||||
get_active_columns(&app.accounts, &app.decks_cache).num_columns(),
|
||||
get_active_columns(ctx.accounts, &app.decks_cache).num_columns(),
|
||||
)
|
||||
.clip(true)
|
||||
.horizontal(|mut strip| {
|
||||
@@ -718,9 +570,9 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) {
|
||||
strip.cell(|ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let side_panel = DesktopSidePanel::new(
|
||||
&app.ndb,
|
||||
&mut app.img_cache,
|
||||
app.accounts.get_selected_account(),
|
||||
ctx.ndb,
|
||||
ctx.img_cache,
|
||||
ctx.accounts.get_selected_account(),
|
||||
&app.decks_cache,
|
||||
)
|
||||
.show(ui);
|
||||
@@ -728,9 +580,9 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) {
|
||||
if side_panel.response.clicked() || side_panel.response.secondary_clicked() {
|
||||
if let Some(action) = DesktopSidePanel::perform_action(
|
||||
&mut app.decks_cache,
|
||||
&app.accounts,
|
||||
ctx.accounts,
|
||||
&mut app.support,
|
||||
&mut app.theme,
|
||||
ctx.theme,
|
||||
side_panel.action,
|
||||
) {
|
||||
side_panel_action = Some(action);
|
||||
@@ -747,15 +599,15 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) {
|
||||
|
||||
let mut save_cols = false;
|
||||
if let Some(action) = side_panel_action {
|
||||
save_cols = save_cols || action.process(app);
|
||||
save_cols = save_cols || action.process(app, ctx);
|
||||
}
|
||||
|
||||
let num_cols = app.columns().num_columns();
|
||||
let num_cols = app.columns(ctx.accounts).num_columns();
|
||||
let mut responses = Vec::with_capacity(num_cols);
|
||||
for col_index in 0..num_cols {
|
||||
strip.cell(|ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
responses.push(nav::render_nav(col_index, app, ui));
|
||||
responses.push(nav::render_nav(col_index, app, ctx, ui));
|
||||
|
||||
// vertical line
|
||||
ui.painter().vline(
|
||||
@@ -769,27 +621,23 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) {
|
||||
}
|
||||
|
||||
for response in responses {
|
||||
let save = response.process_render_nav_response(app);
|
||||
let save = response.process_render_nav_response(app, ctx);
|
||||
save_cols = save_cols || save;
|
||||
}
|
||||
|
||||
if save_cols {
|
||||
storage::save_decks_cache(&app.path, &app.decks_cache);
|
||||
storage::save_decks_cache(ctx.path, &app.decks_cache);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl eframe::App for Damus {
|
||||
/// Called by the frame work to save state before shutdown.
|
||||
fn save(&mut self, _storage: &mut dyn eframe::Storage) {
|
||||
//eframe::set_value(storage, eframe::APP_KEY, self);
|
||||
}
|
||||
|
||||
/// Called each time the UI needs repainting, which may be many times per second.
|
||||
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
self.frame_history
|
||||
impl notedeck::App for Damus {
|
||||
fn update(&mut self, ctx: &mut AppContext<'_>) {
|
||||
/*
|
||||
self.app
|
||||
.frame_history
|
||||
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||
*/
|
||||
|
||||
#[cfg(feature = "profiling")]
|
||||
puffin::GlobalProfiler::lock().new_frame();
|
||||
|
||||
@@ -1,83 +1 @@
|
||||
use crate::{
|
||||
app_size_handler::AppSizeHandler,
|
||||
app_style::{add_custom_style, dark_mode, light_mode},
|
||||
fonts::setup_fonts,
|
||||
storage::DataPath,
|
||||
};
|
||||
|
||||
use eframe::NativeOptions;
|
||||
|
||||
//pub const UI_SCALE_FACTOR: f32 = 0.2;
|
||||
|
||||
pub fn generate_native_options(paths: DataPath) -> NativeOptions {
|
||||
let window_builder = Box::new(move |builder: egui::ViewportBuilder| {
|
||||
let builder = builder
|
||||
.with_fullsize_content_view(true)
|
||||
.with_titlebar_shown(false)
|
||||
.with_title_shown(false)
|
||||
.with_icon(std::sync::Arc::new(
|
||||
eframe::icon_data::from_png_bytes(app_icon()).expect("icon"),
|
||||
));
|
||||
|
||||
if let Some(window_size) = AppSizeHandler::new(&paths).get_app_size() {
|
||||
builder.with_inner_size(window_size)
|
||||
} else {
|
||||
builder
|
||||
}
|
||||
});
|
||||
|
||||
eframe::NativeOptions {
|
||||
window_builder: Some(window_builder),
|
||||
viewport: egui::ViewportBuilder::default().with_icon(std::sync::Arc::new(
|
||||
eframe::icon_data::from_png_bytes(app_icon()).expect("icon"),
|
||||
)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_native_options_with_builder_modifiers(
|
||||
apply_builder_modifiers: fn(egui::ViewportBuilder) -> egui::ViewportBuilder,
|
||||
) -> NativeOptions {
|
||||
let window_builder =
|
||||
Box::new(move |builder: egui::ViewportBuilder| apply_builder_modifiers(builder));
|
||||
|
||||
eframe::NativeOptions {
|
||||
window_builder: Some(window_builder),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_icon() -> &'static [u8; 271986] {
|
||||
std::include_bytes!("../assets/damus-app-icon.png")
|
||||
}
|
||||
|
||||
pub fn generate_mobile_emulator_native_options() -> eframe::NativeOptions {
|
||||
generate_native_options_with_builder_modifiers(|builder| {
|
||||
builder
|
||||
.with_fullsize_content_view(true)
|
||||
.with_titlebar_shown(false)
|
||||
.with_title_shown(false)
|
||||
.with_inner_size([405.0, 915.0])
|
||||
.with_icon(eframe::icon_data::from_png_bytes(app_icon()).expect("icon"))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn setup_cc(ctx: &egui::Context, is_mobile: bool, light: bool) {
|
||||
setup_fonts(ctx);
|
||||
|
||||
//ctx.set_pixels_per_point(ctx.pixels_per_point() + UI_SCALE_FACTOR);
|
||||
//ctx.set_pixels_per_point(1.0);
|
||||
//
|
||||
//
|
||||
//ctx.tessellation_options_mut(|to| to.feathering = false);
|
||||
|
||||
egui_extras::install_image_loaders(ctx);
|
||||
|
||||
if light {
|
||||
ctx.set_visuals(light_mode())
|
||||
} else {
|
||||
ctx.set_visuals(dark_mode(is_mobile));
|
||||
}
|
||||
|
||||
ctx.all_styles_mut(|style| add_custom_style(is_mobile, style));
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use egui::Context;
|
||||
use tracing::info;
|
||||
|
||||
use crate::storage::{write_file, DataPath, DataPathType, Directory};
|
||||
|
||||
pub struct AppSizeHandler {
|
||||
directory: Directory,
|
||||
saved_size: Option<egui::Vec2>,
|
||||
last_saved: Instant,
|
||||
}
|
||||
|
||||
static FILE_NAME: &str = "app_size.json";
|
||||
static DELAY: Duration = Duration::from_millis(500);
|
||||
|
||||
impl AppSizeHandler {
|
||||
pub fn new(path: &DataPath) -> Self {
|
||||
let directory = Directory::new(path.path(DataPathType::Setting));
|
||||
|
||||
Self {
|
||||
directory,
|
||||
saved_size: None,
|
||||
last_saved: Instant::now() - DELAY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_save_app_size(&mut self, ctx: &Context) {
|
||||
// There doesn't seem to be a way to check if user is resizing window, so if the rect is different than last saved, we'll wait DELAY before saving again to avoid spamming io
|
||||
if self.last_saved.elapsed() >= DELAY {
|
||||
internal_try_save_app_size(&self.directory, &mut self.saved_size, ctx);
|
||||
self.last_saved = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_app_size(&self) -> Option<egui::Vec2> {
|
||||
if self.saved_size.is_some() {
|
||||
return self.saved_size;
|
||||
}
|
||||
|
||||
if let Ok(file_contents) = self.directory.get_file(FILE_NAME.to_owned()) {
|
||||
if let Ok(rect) = serde_json::from_str::<egui::Vec2>(&file_contents) {
|
||||
return Some(rect);
|
||||
}
|
||||
} else {
|
||||
info!("Could not find {}", FILE_NAME);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_try_save_app_size(
|
||||
interactor: &Directory,
|
||||
maybe_saved_size: &mut Option<egui::Vec2>,
|
||||
ctx: &Context,
|
||||
) {
|
||||
let cur_size = ctx.input(|i| i.screen_rect.size());
|
||||
if let Some(saved_size) = maybe_saved_size {
|
||||
if cur_size != *saved_size {
|
||||
try_save_size(interactor, cur_size, maybe_saved_size);
|
||||
}
|
||||
} else {
|
||||
try_save_size(interactor, cur_size, maybe_saved_size);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_save_size(
|
||||
interactor: &Directory,
|
||||
cur_size: egui::Vec2,
|
||||
maybe_saved_size: &mut Option<egui::Vec2>,
|
||||
) {
|
||||
if let Ok(serialized_rect) = serde_json::to_string(&cur_size) {
|
||||
if write_file(
|
||||
&interactor.file_path,
|
||||
FILE_NAME.to_owned(),
|
||||
&serialized_rect,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
info!("wrote size {}", cur_size,);
|
||||
*maybe_saved_size = Some(cur_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,210 +1,6 @@
|
||||
use crate::{
|
||||
colors::{desktop_dark_color_theme, light_color_theme, mobile_dark_color_theme, ColorTheme},
|
||||
fonts::NamedFontFamily,
|
||||
ui::is_narrow,
|
||||
};
|
||||
use egui::{
|
||||
epaint::Shadow,
|
||||
style::{Interaction, Selection, WidgetVisuals, Widgets},
|
||||
FontFamily, FontId, Rounding, Stroke, Style, TextStyle, Visuals,
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::EnumIter;
|
||||
use egui::{FontFamily, FontId};
|
||||
|
||||
const WIDGET_ROUNDING: Rounding = Rounding::same(8.0);
|
||||
|
||||
pub fn light_mode() -> Visuals {
|
||||
create_themed_visuals(light_color_theme(), Visuals::light())
|
||||
}
|
||||
|
||||
pub fn dark_mode(mobile: bool) -> Visuals {
|
||||
create_themed_visuals(
|
||||
if mobile {
|
||||
mobile_dark_color_theme()
|
||||
} else {
|
||||
desktop_dark_color_theme()
|
||||
},
|
||||
Visuals::dark(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create custom text sizes for any FontSizes
|
||||
pub fn add_custom_style(is_mobile: bool, style: &mut Style) {
|
||||
let font_size = if is_mobile {
|
||||
mobile_font_size
|
||||
} else {
|
||||
desktop_font_size
|
||||
};
|
||||
style.text_styles = NotedeckTextStyle::iter()
|
||||
.map(|text_style| {
|
||||
(
|
||||
text_style.text_style(),
|
||||
FontId::new(font_size(&text_style), text_style.font_family()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
style.interaction = Interaction {
|
||||
tooltip_delay: 0.1,
|
||||
show_tooltips_only_when_still: false,
|
||||
..Interaction::default()
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
style.debug.show_interactive_widgets = true;
|
||||
style.debug.debug_on_hover_with_all_modifiers = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
||||
match text_style {
|
||||
NotedeckTextStyle::Heading => 48.0,
|
||||
NotedeckTextStyle::Heading2 => 24.0,
|
||||
NotedeckTextStyle::Heading3 => 20.0,
|
||||
NotedeckTextStyle::Heading4 => 14.0,
|
||||
NotedeckTextStyle::Body => 16.0,
|
||||
NotedeckTextStyle::Monospace => 13.0,
|
||||
NotedeckTextStyle::Button => 13.0,
|
||||
NotedeckTextStyle::Small => 12.0,
|
||||
NotedeckTextStyle::Tiny => 10.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 {
|
||||
// TODO: tweak text sizes for optimal mobile viewing
|
||||
match text_style {
|
||||
NotedeckTextStyle::Heading => 48.0,
|
||||
NotedeckTextStyle::Heading2 => 24.0,
|
||||
NotedeckTextStyle::Heading3 => 20.0,
|
||||
NotedeckTextStyle::Heading4 => 14.0,
|
||||
NotedeckTextStyle::Body => 13.0,
|
||||
NotedeckTextStyle::Monospace => 13.0,
|
||||
NotedeckTextStyle::Button => 13.0,
|
||||
NotedeckTextStyle::Small => 12.0,
|
||||
NotedeckTextStyle::Tiny => 10.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32 {
|
||||
if is_narrow(ctx) {
|
||||
mobile_font_size(text_style)
|
||||
} else {
|
||||
desktop_font_size(text_style)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, EnumIter)]
|
||||
pub enum NotedeckTextStyle {
|
||||
Heading,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Heading4,
|
||||
Body,
|
||||
Monospace,
|
||||
Button,
|
||||
Small,
|
||||
Tiny,
|
||||
}
|
||||
|
||||
impl NotedeckTextStyle {
|
||||
pub fn text_style(&self) -> TextStyle {
|
||||
match self {
|
||||
Self::Heading => TextStyle::Heading,
|
||||
Self::Heading2 => TextStyle::Name("Heading2".into()),
|
||||
Self::Heading3 => TextStyle::Name("Heading3".into()),
|
||||
Self::Heading4 => TextStyle::Name("Heading4".into()),
|
||||
Self::Body => TextStyle::Body,
|
||||
Self::Monospace => TextStyle::Monospace,
|
||||
Self::Button => TextStyle::Button,
|
||||
Self::Small => TextStyle::Small,
|
||||
Self::Tiny => TextStyle::Name("Tiny".into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font_family(&self) -> FontFamily {
|
||||
match self {
|
||||
Self::Heading => FontFamily::Proportional,
|
||||
Self::Heading2 => FontFamily::Proportional,
|
||||
Self::Heading3 => FontFamily::Proportional,
|
||||
Self::Heading4 => FontFamily::Proportional,
|
||||
Self::Body => FontFamily::Proportional,
|
||||
Self::Monospace => FontFamily::Monospace,
|
||||
Self::Button => FontFamily::Proportional,
|
||||
Self::Small => FontFamily::Proportional,
|
||||
Self::Tiny => FontFamily::Proportional,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_themed_visuals(theme: ColorTheme, default: Visuals) -> Visuals {
|
||||
Visuals {
|
||||
hyperlink_color: theme.hyperlink_color,
|
||||
override_text_color: Some(theme.text_color),
|
||||
panel_fill: theme.panel_fill,
|
||||
selection: Selection {
|
||||
bg_fill: theme.selection_color,
|
||||
stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: theme.selection_color,
|
||||
},
|
||||
},
|
||||
warn_fg_color: theme.warn_fg_color,
|
||||
widgets: Widgets {
|
||||
noninteractive: WidgetVisuals {
|
||||
bg_fill: theme.noninteractive_bg_fill,
|
||||
weak_bg_fill: theme.noninteractive_weak_bg_fill,
|
||||
bg_stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: theme.noninteractive_bg_stroke_color,
|
||||
},
|
||||
fg_stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: theme.noninteractive_fg_stroke_color,
|
||||
},
|
||||
rounding: WIDGET_ROUNDING,
|
||||
..default.widgets.noninteractive
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
bg_fill: theme.inactive_bg_fill,
|
||||
weak_bg_fill: theme.inactive_weak_bg_fill,
|
||||
bg_stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: theme.inactive_bg_stroke_color,
|
||||
},
|
||||
rounding: WIDGET_ROUNDING,
|
||||
..default.widgets.inactive
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
rounding: WIDGET_ROUNDING,
|
||||
..default.widgets.hovered
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
rounding: WIDGET_ROUNDING,
|
||||
..default.widgets.active
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
..default.widgets.open
|
||||
},
|
||||
},
|
||||
extreme_bg_color: theme.extreme_bg_color,
|
||||
error_fg_color: theme.err_fg_color,
|
||||
window_rounding: Rounding::same(8.0),
|
||||
window_fill: theme.window_fill,
|
||||
window_shadow: Shadow {
|
||||
offset: [0.0, 8.0].into(),
|
||||
blur: 24.0,
|
||||
spread: 0.0,
|
||||
color: egui::Color32::from_rgba_unmultiplied(0x6D, 0x6D, 0x6D, 0x14),
|
||||
},
|
||||
window_stroke: Stroke {
|
||||
width: 1.0,
|
||||
color: theme.window_stroke_color,
|
||||
},
|
||||
image_loading_spinners: false,
|
||||
..default
|
||||
}
|
||||
}
|
||||
use notedeck::fonts::NamedFontFamily;
|
||||
|
||||
pub static DECK_ICON_SIZE: f32 = 24.0;
|
||||
|
||||
|
||||
@@ -1,37 +1,22 @@
|
||||
use crate::filter::FilterState;
|
||||
use notedeck::FilterState;
|
||||
|
||||
use crate::timeline::{PubkeySource, Timeline, TimelineKind};
|
||||
use enostr::{Filter, Keypair, Pubkey, SecretKey};
|
||||
use enostr::{Filter, Pubkey};
|
||||
use nostrdb::Ndb;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub struct Args {
|
||||
pub struct ColumnsArgs {
|
||||
pub columns: Vec<ArgColumn>,
|
||||
pub relays: Vec<String>,
|
||||
pub is_mobile: Option<bool>,
|
||||
pub keys: Vec<Keypair>,
|
||||
pub since_optimize: bool,
|
||||
pub light: bool,
|
||||
pub debug: bool,
|
||||
pub textmode: bool,
|
||||
pub use_keystore: bool,
|
||||
pub dbpath: Option<String>,
|
||||
pub datapath: Option<String>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
impl ColumnsArgs {
|
||||
pub fn parse(args: &[String]) -> Self {
|
||||
let mut res = Args {
|
||||
let mut res = Self {
|
||||
columns: vec![],
|
||||
relays: vec![],
|
||||
is_mobile: None,
|
||||
keys: vec![],
|
||||
light: false,
|
||||
since_optimize: true,
|
||||
debug: false,
|
||||
textmode: false,
|
||||
use_keystore: true,
|
||||
dbpath: None,
|
||||
datapath: None,
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
@@ -39,50 +24,8 @@ impl Args {
|
||||
while i < len {
|
||||
let arg = &args[i];
|
||||
|
||||
if arg == "--mobile" {
|
||||
res.is_mobile = Some(true);
|
||||
} else if arg == "--light" {
|
||||
res.light = true;
|
||||
} else if arg == "--dark" {
|
||||
res.light = false;
|
||||
} else if arg == "--debug" {
|
||||
res.debug = true;
|
||||
} else if arg == "--textmode" {
|
||||
if arg == "--textmode" {
|
||||
res.textmode = true;
|
||||
} else if arg == "--pub" || arg == "--npub" {
|
||||
i += 1;
|
||||
let pubstr = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("sec argument missing?");
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(pk) = Pubkey::parse(pubstr) {
|
||||
res.keys.push(Keypair::only_pubkey(pk));
|
||||
} else {
|
||||
error!(
|
||||
"failed to parse {} argument. Make sure to use hex or npub.",
|
||||
arg
|
||||
);
|
||||
}
|
||||
} else if arg == "--sec" || arg == "--nsec" {
|
||||
i += 1;
|
||||
let secstr = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("sec argument missing?");
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(sec) = SecretKey::parse(secstr) {
|
||||
res.keys.push(Keypair::from_secret(sec));
|
||||
} else {
|
||||
error!(
|
||||
"failed to parse {} argument. Make sure to use hex or nsec.",
|
||||
arg
|
||||
);
|
||||
}
|
||||
} else if arg == "--no-since-optimize" {
|
||||
res.since_optimize = false;
|
||||
} else if arg == "--filter" {
|
||||
@@ -99,33 +42,6 @@ impl Args {
|
||||
} else {
|
||||
error!("failed to parse filter '{}'", filter);
|
||||
}
|
||||
} else if arg == "--dbpath" {
|
||||
i += 1;
|
||||
let path = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("dbpath argument missing?");
|
||||
continue;
|
||||
};
|
||||
res.dbpath = Some(path.clone());
|
||||
} else if arg == "--datapath" {
|
||||
i += 1;
|
||||
let path = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("datapath argument missing?");
|
||||
continue;
|
||||
};
|
||||
res.datapath = Some(path.clone());
|
||||
} else if arg == "-r" || arg == "--relay" {
|
||||
i += 1;
|
||||
let relay = if let Some(next_arg) = args.get(i) {
|
||||
next_arg
|
||||
} else {
|
||||
error!("relay argument missing?");
|
||||
continue;
|
||||
};
|
||||
res.relays.push(relay.clone());
|
||||
} else if arg == "--column" || arg == "-c" {
|
||||
i += 1;
|
||||
let column_name = if let Some(next_arg) = args.get(i) {
|
||||
@@ -212,8 +128,6 @@ impl Args {
|
||||
} else {
|
||||
error!("failed to parse filter in '{}'", filter_file);
|
||||
}
|
||||
} else if arg == "--no-keystore" {
|
||||
res.use_keystore = false;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
@@ -242,82 +156,3 @@ impl ArgColumn {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::Damus;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn create_tmp_dir() -> PathBuf {
|
||||
tempfile::TempDir::new()
|
||||
.expect("tmp path")
|
||||
.path()
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn rmrf(path: impl AsRef<Path>) {
|
||||
let _ = std::fs::remove_dir_all(path);
|
||||
}
|
||||
|
||||
/// Ensure dbpath actually sets the dbpath correctly.
|
||||
#[tokio::test]
|
||||
async fn test_dbpath() {
|
||||
let datapath = create_tmp_dir();
|
||||
let dbpath = create_tmp_dir();
|
||||
let args = vec![
|
||||
"--datapath",
|
||||
&datapath.to_str().unwrap(),
|
||||
"--dbpath",
|
||||
&dbpath.to_str().unwrap(),
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
let _app = Damus::new(&ctx, &datapath, args);
|
||||
|
||||
assert!(Path::new(&dbpath.join("data.mdb")).exists());
|
||||
assert!(Path::new(&dbpath.join("lock.mdb")).exists());
|
||||
assert!(!Path::new(&datapath.join("db")).exists());
|
||||
|
||||
rmrf(datapath);
|
||||
rmrf(dbpath);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_column_args() {
|
||||
let tmpdir = create_tmp_dir();
|
||||
let npub = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s";
|
||||
let args = vec![
|
||||
"--no-keystore",
|
||||
"--pub",
|
||||
npub,
|
||||
"-c",
|
||||
"notifications",
|
||||
"-c",
|
||||
"contacts",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
let app = Damus::new(&ctx, &tmpdir, args);
|
||||
|
||||
assert_eq!(app.columns().columns().len(), 2);
|
||||
|
||||
let tl1 = app.columns().column(0).router().top().timeline_id();
|
||||
let tl2 = app.columns().column(1).router().top().timeline_id();
|
||||
|
||||
assert_eq!(tl1.is_some(), true);
|
||||
assert_eq!(tl2.is_some(), true);
|
||||
|
||||
let timelines = app.columns().timelines();
|
||||
assert!(timelines[0].kind.is_notifications());
|
||||
assert!(timelines[1].kind.is_contacts());
|
||||
|
||||
rmrf(tmpdir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +1,5 @@
|
||||
use egui::Color32;
|
||||
|
||||
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
|
||||
const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD);
|
||||
// TODO: This should not be exposed publicly
|
||||
pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9);
|
||||
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
|
||||
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
|
||||
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
|
||||
const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A);
|
||||
const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A);
|
||||
|
||||
// BACKGROUNDS
|
||||
const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39);
|
||||
const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F);
|
||||
const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C);
|
||||
const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25);
|
||||
const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44);
|
||||
|
||||
const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8);
|
||||
const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78%
|
||||
pub const MID_GRAY: Color32 = Color32::from_rgb(0xbd, 0xbd, 0xbd);
|
||||
const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65%
|
||||
const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54%
|
||||
pub const ALMOST_WHITE: Color32 = Color32::from_rgb(0xFA, 0xFA, 0xFA);
|
||||
|
||||
pub struct ColorTheme {
|
||||
// VISUALS
|
||||
pub panel_fill: Color32,
|
||||
pub extreme_bg_color: Color32,
|
||||
pub text_color: Color32,
|
||||
pub err_fg_color: Color32,
|
||||
pub warn_fg_color: Color32,
|
||||
pub hyperlink_color: Color32,
|
||||
pub selection_color: Color32,
|
||||
|
||||
// WINDOW
|
||||
pub window_fill: Color32,
|
||||
pub window_stroke_color: Color32,
|
||||
|
||||
// NONINTERACTIVE WIDGET
|
||||
pub noninteractive_bg_fill: Color32,
|
||||
pub noninteractive_weak_bg_fill: Color32,
|
||||
pub noninteractive_bg_stroke_color: Color32,
|
||||
pub noninteractive_fg_stroke_color: Color32,
|
||||
|
||||
// INACTIVE WIDGET
|
||||
pub inactive_bg_stroke_color: Color32,
|
||||
pub inactive_bg_fill: Color32,
|
||||
pub inactive_weak_bg_fill: Color32,
|
||||
}
|
||||
|
||||
pub fn desktop_dark_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
// VISUALS
|
||||
panel_fill: DARKER_BG,
|
||||
extreme_bg_color: DARK_ISH_BG,
|
||||
text_color: Color32::WHITE,
|
||||
err_fg_color: RED_700,
|
||||
warn_fg_color: ORANGE_700,
|
||||
hyperlink_color: PURPLE,
|
||||
selection_color: PURPLE_ALT,
|
||||
|
||||
// WINDOW
|
||||
window_fill: DARK_ISH_BG,
|
||||
window_stroke_color: DARK_BG,
|
||||
|
||||
// NONINTERACTIVE WIDGET
|
||||
noninteractive_bg_fill: DARK_ISH_BG,
|
||||
noninteractive_weak_bg_fill: DARK_BG,
|
||||
noninteractive_bg_stroke_color: SEMI_DARKER_BG,
|
||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||
|
||||
// INACTIVE WIDGET
|
||||
inactive_bg_stroke_color: SEMI_DARKER_BG,
|
||||
inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25),
|
||||
inactive_weak_bg_fill: SEMI_DARK_BG,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mobile_dark_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
panel_fill: Color32::BLACK,
|
||||
noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
||||
..desktop_dark_color_theme()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light_color_theme() -> ColorTheme {
|
||||
ColorTheme {
|
||||
// VISUALS
|
||||
panel_fill: Color32::WHITE,
|
||||
extreme_bg_color: LIGHTER_GRAY,
|
||||
text_color: BLACK,
|
||||
err_fg_color: RED_700,
|
||||
warn_fg_color: ORANGE_700,
|
||||
hyperlink_color: PURPLE,
|
||||
selection_color: PURPLE_ALT,
|
||||
|
||||
// WINDOW
|
||||
window_fill: Color32::WHITE,
|
||||
window_stroke_color: DARKER_GRAY,
|
||||
|
||||
// NONINTERACTIVE WIDGET
|
||||
noninteractive_bg_fill: Color32::WHITE,
|
||||
noninteractive_weak_bg_fill: LIGHTER_GRAY,
|
||||
noninteractive_bg_stroke_color: LIGHT_GRAY,
|
||||
noninteractive_fg_stroke_color: GRAY_SECONDARY,
|
||||
|
||||
// INACTIVE WIDGET
|
||||
inactive_bg_stroke_color: EVEN_DARKER_GRAY,
|
||||
inactive_bg_fill: LIGHT_GRAY,
|
||||
inactive_weak_bg_fill: EVEN_DARKER_GRAY,
|
||||
}
|
||||
}
|
||||
pub const MID_GRAY: Color32 = Color32::from_rgb(0xbd, 0xbd, 0xbd);
|
||||
pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9);
|
||||
|
||||
@@ -1,126 +1,31 @@
|
||||
use std::{fmt, io};
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum FilterError {
|
||||
EmptyContactList,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum SubscriptionError {
|
||||
//#[error("No active subscriptions")]
|
||||
NoActive,
|
||||
|
||||
/// When a timeline has an unexpected number
|
||||
/// of active subscriptions. Should only happen if there
|
||||
/// is a bug in notedeck
|
||||
//#[error("Unexpected subscription count")]
|
||||
UnexpectedSubscriptionCount(i32),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn unexpected_sub_count(c: i32) -> Self {
|
||||
Error::SubscriptionError(SubscriptionError::UnexpectedSubscriptionCount(c))
|
||||
}
|
||||
|
||||
pub fn no_active_sub() -> Self {
|
||||
Error::SubscriptionError(SubscriptionError::NoActive)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SubscriptionError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::NoActive => write!(f, "No active subscriptions"),
|
||||
Self::UnexpectedSubscriptionCount(c) => {
|
||||
write!(f, "Unexpected subscription count: {}", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("timeline not found")]
|
||||
TimelineNotFound,
|
||||
|
||||
#[error("load failed")]
|
||||
LoadFailed,
|
||||
SubscriptionError(SubscriptionError),
|
||||
Filter(FilterError),
|
||||
Io(io::Error),
|
||||
Nostr(enostr::Error),
|
||||
Ndb(nostrdb::Error),
|
||||
Image(image::error::ImageError),
|
||||
|
||||
#[error("network error: {0}")]
|
||||
Nostr(#[from] enostr::Error),
|
||||
|
||||
#[error("database error: {0}")]
|
||||
Ndb(#[from] nostrdb::Error),
|
||||
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("notedeck app error: {0}")]
|
||||
App(#[from] notedeck::Error),
|
||||
|
||||
#[error("generic error: {0}")]
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn empty_contact_list() -> Self {
|
||||
Error::Filter(FilterError::EmptyContactList)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FilterError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::EmptyContactList => {
|
||||
write!(f, "empty contact list")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::SubscriptionError(e) => {
|
||||
write!(f, "{e}")
|
||||
}
|
||||
Self::TimelineNotFound => write!(f, "Timeline not found"),
|
||||
Self::LoadFailed => {
|
||||
write!(f, "load failed")
|
||||
}
|
||||
Self::Filter(e) => {
|
||||
write!(f, "{e}")
|
||||
}
|
||||
Self::Nostr(e) => write!(f, "{e}"),
|
||||
Self::Ndb(e) => write!(f, "{e}"),
|
||||
Self::Image(e) => write!(f, "{e}"),
|
||||
Self::Generic(e) => write!(f, "{e}"),
|
||||
Self::Io(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Error::Generic(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nostrdb::Error> for Error {
|
||||
fn from(e: nostrdb::Error) -> Self {
|
||||
Error::Ndb(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<image::error::ImageError> for Error {
|
||||
fn from(err: image::error::ImageError) -> Self {
|
||||
Error::Image(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<enostr::Error> for Error {
|
||||
fn from(err: enostr::Error) -> Self {
|
||||
Error::Nostr(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Self {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FilterError> for Error {
|
||||
fn from(err: FilterError) -> Self {
|
||||
Error::Filter(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
use crate::error::{Error, FilterError};
|
||||
use crate::note::NoteRef;
|
||||
use crate::Result;
|
||||
use nostrdb::{Filter, FilterBuilder, Note, Subscription};
|
||||
use std::collections::HashMap;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// A unified subscription has a local and remote component. The remote subid
|
||||
/// tracks data received remotely, and local
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UnifiedSubscription {
|
||||
pub local: Subscription,
|
||||
pub remote: String,
|
||||
}
|
||||
|
||||
/// Each relay can have a different filter state. For example, some
|
||||
/// relays may have the contact list, some may not. Let's capture all of
|
||||
/// these states so that some relays don't stop the states of other
|
||||
/// relays.
|
||||
#[derive(Debug)]
|
||||
pub struct FilterStates {
|
||||
pub initial_state: FilterState,
|
||||
pub states: HashMap<String, FilterState>,
|
||||
}
|
||||
|
||||
impl FilterStates {
|
||||
pub fn get(&mut self, relay: &str) -> &FilterState {
|
||||
// if our initial state is ready, then just use that
|
||||
if let FilterState::Ready(_) = self.initial_state {
|
||||
&self.initial_state
|
||||
} else {
|
||||
// otherwise we look at relay states
|
||||
if !self.states.contains_key(relay) {
|
||||
self.states
|
||||
.insert(relay.to_string(), self.initial_state.clone());
|
||||
}
|
||||
self.states.get(relay).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_any_gotremote(&self) -> Option<(&str, Subscription)> {
|
||||
for (k, v) in self.states.iter() {
|
||||
if let FilterState::GotRemote(sub) = v {
|
||||
return Some((k, *sub));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_any_ready(&self) -> Option<&Vec<Filter>> {
|
||||
if let FilterState::Ready(fs) = &self.initial_state {
|
||||
Some(fs)
|
||||
} else {
|
||||
for (_k, v) in self.states.iter() {
|
||||
if let FilterState::Ready(ref fs) = v {
|
||||
return Some(fs);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(initial_state: FilterState) -> Self {
|
||||
Self {
|
||||
initial_state,
|
||||
states: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_relay_state(&mut self, relay: String, state: FilterState) {
|
||||
if self.states.contains_key(&relay) {
|
||||
let current_state = self.states.get(&relay).unwrap();
|
||||
debug!(
|
||||
"set_relay_state: {:?} -> {:?} on {}",
|
||||
current_state, state, &relay,
|
||||
);
|
||||
}
|
||||
self.states.insert(relay, state);
|
||||
}
|
||||
}
|
||||
|
||||
/// We may need to fetch some data from relays before our filter is ready.
|
||||
/// [`FilterState`] tracks this.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FilterState {
|
||||
NeedsRemote(Vec<Filter>),
|
||||
FetchingRemote(UnifiedSubscription),
|
||||
GotRemote(Subscription),
|
||||
Ready(Vec<Filter>),
|
||||
Broken(FilterError),
|
||||
}
|
||||
|
||||
impl FilterState {
|
||||
/// We tried to fetch a filter but we wither got no data or the data
|
||||
/// was corrupted, preventing us from getting to the Ready state.
|
||||
/// Just mark the timeline as broken so that we can signal to the
|
||||
/// user that something went wrong
|
||||
pub fn broken(reason: FilterError) -> Self {
|
||||
Self::Broken(reason)
|
||||
}
|
||||
|
||||
/// The filter is ready
|
||||
pub fn ready(filter: Vec<Filter>) -> Self {
|
||||
Self::Ready(filter)
|
||||
}
|
||||
|
||||
/// We need some data from relays before we can continue. Example:
|
||||
/// for home timelines where we don't have a contact list yet. We
|
||||
/// need to fetch the contact list before we have the right timeline
|
||||
/// filter.
|
||||
pub fn needs_remote(filter: Vec<Filter>) -> Self {
|
||||
Self::NeedsRemote(filter)
|
||||
}
|
||||
|
||||
/// We got the remote data. Local data should be available to build
|
||||
/// the filter for the [`FilterState::Ready`] state
|
||||
pub fn got_remote(local_sub: Subscription) -> Self {
|
||||
Self::GotRemote(local_sub)
|
||||
}
|
||||
|
||||
/// We have sent off a remote subscription to get data needed for the
|
||||
/// filter. The string is the subscription id
|
||||
pub fn fetching_remote(sub_id: String, local_sub: Subscription) -> Self {
|
||||
let unified_sub = UnifiedSubscription {
|
||||
local: local_sub,
|
||||
remote: sub_id,
|
||||
};
|
||||
Self::FetchingRemote(unified_sub)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_since_optimize(limit: u64, num_notes: usize) -> bool {
|
||||
// rough heuristic for bailing since optimization if we don't have enough notes
|
||||
limit as usize <= num_notes
|
||||
}
|
||||
|
||||
pub fn since_optimize_filter_with(filter: Filter, notes: &[NoteRef], since_gap: u64) -> Filter {
|
||||
// Get the latest entry in the events
|
||||
if notes.is_empty() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
// get the latest note
|
||||
let latest = notes[0];
|
||||
let since = latest.created_at - since_gap;
|
||||
|
||||
filter.since_mut(since)
|
||||
}
|
||||
|
||||
pub fn since_optimize_filter(filter: Filter, notes: &[NoteRef]) -> Filter {
|
||||
since_optimize_filter_with(filter, notes, 60)
|
||||
}
|
||||
|
||||
pub fn default_limit() -> u64 {
|
||||
500
|
||||
}
|
||||
|
||||
pub fn default_remote_limit() -> u64 {
|
||||
250
|
||||
}
|
||||
|
||||
pub struct FilteredTags {
|
||||
pub authors: Option<FilterBuilder>,
|
||||
pub hashtags: Option<FilterBuilder>,
|
||||
}
|
||||
|
||||
impl FilteredTags {
|
||||
pub fn into_follow_filter(self) -> Vec<Filter> {
|
||||
self.into_filter([1], default_limit())
|
||||
}
|
||||
|
||||
// TODO: make this more general
|
||||
pub fn into_filter<I>(self, kinds: I, limit: u64) -> Vec<Filter>
|
||||
where
|
||||
I: IntoIterator<Item = u64> + Copy,
|
||||
{
|
||||
let mut filters: Vec<Filter> = Vec::with_capacity(2);
|
||||
|
||||
if let Some(authors) = self.authors {
|
||||
filters.push(authors.kinds(kinds).limit(limit).build())
|
||||
}
|
||||
|
||||
if let Some(hashtags) = self.hashtags {
|
||||
filters.push(hashtags.kinds(kinds).limit(limit).build())
|
||||
}
|
||||
|
||||
filters
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a filter from tags. This can be used to create a filter
|
||||
/// from a contact list
|
||||
pub fn filter_from_tags(note: &Note) -> Result<FilteredTags> {
|
||||
let mut author_filter = Filter::new();
|
||||
let mut hashtag_filter = Filter::new();
|
||||
let mut author_res: Option<FilterBuilder> = None;
|
||||
let mut hashtag_res: Option<FilterBuilder> = None;
|
||||
let mut author_count = 0i32;
|
||||
let mut hashtag_count = 0i32;
|
||||
|
||||
let tags = note.tags();
|
||||
|
||||
author_filter.start_authors_field()?;
|
||||
hashtag_filter.start_tags_field('t')?;
|
||||
|
||||
for tag in tags {
|
||||
if tag.count() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let t = if let Some(t) = tag.get_unchecked(0).variant().str() {
|
||||
t
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if t == "p" {
|
||||
let author = if let Some(author) = tag.get_unchecked(1).variant().id() {
|
||||
author
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
author_filter.add_id_element(author)?;
|
||||
author_count += 1;
|
||||
} else if t == "t" {
|
||||
let hashtag = if let Some(hashtag) = tag.get_unchecked(1).variant().str() {
|
||||
hashtag
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
hashtag_filter.add_str_element(hashtag)?;
|
||||
hashtag_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
author_filter.end_field();
|
||||
hashtag_filter.end_field();
|
||||
|
||||
if author_count == 0 && hashtag_count == 0 {
|
||||
warn!("no authors or hashtags found in contact list");
|
||||
return Err(Error::empty_contact_list());
|
||||
}
|
||||
|
||||
debug!(
|
||||
"adding {} authors and {} hashtags to contact filter",
|
||||
author_count, hashtag_count
|
||||
);
|
||||
|
||||
// if we hit these ooms, we need to expand filter buffer size
|
||||
if author_count > 0 {
|
||||
author_res = Some(author_filter)
|
||||
}
|
||||
|
||||
if hashtag_count > 0 {
|
||||
hashtag_res = Some(hashtag_filter)
|
||||
}
|
||||
|
||||
Ok(FilteredTags {
|
||||
authors: author_res,
|
||||
hashtags: hashtag_res,
|
||||
})
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
use egui::{FontData, FontDefinitions, FontTweak};
|
||||
use std::collections::BTreeMap;
|
||||
use tracing::debug;
|
||||
|
||||
pub enum NamedFontFamily {
|
||||
Medium,
|
||||
Bold,
|
||||
Emoji,
|
||||
}
|
||||
|
||||
impl NamedFontFamily {
|
||||
pub fn as_str(&mut self) -> &'static str {
|
||||
match self {
|
||||
Self::Bold => "bold",
|
||||
Self::Medium => "medium",
|
||||
Self::Emoji => "emoji",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_family(&mut self) -> egui::FontFamily {
|
||||
egui::FontFamily::Name(self.as_str().into())
|
||||
}
|
||||
}
|
||||
|
||||
// Use gossip's approach to font loading. This includes japanese fonts
|
||||
// for rending stuff from japanese users.
|
||||
pub fn setup_fonts(ctx: &egui::Context) {
|
||||
let mut font_data: BTreeMap<String, FontData> = BTreeMap::new();
|
||||
let mut families = BTreeMap::new();
|
||||
|
||||
font_data.insert(
|
||||
"Onest".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/onest/OnestRegular1602-hint.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"OnestMedium".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/onest/OnestMedium1602-hint.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"DejaVuSans".to_owned(),
|
||||
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"OnestBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/onest/OnestBold1602-hint.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
/*
|
||||
font_data.insert(
|
||||
"DejaVuSansBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||
)),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"DejaVuSans".to_owned(),
|
||||
FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")),
|
||||
);
|
||||
font_data.insert(
|
||||
"DejaVuSansBold".to_owned(),
|
||||
FontData::from_static(include_bytes!(
|
||||
"../assets/fonts/DejaVuSans-Bold-SansEmoji.ttf"
|
||||
)),
|
||||
);
|
||||
*/
|
||||
|
||||
font_data.insert(
|
||||
"Inconsolata".to_owned(),
|
||||
FontData::from_static(include_bytes!("../assets/fonts/Inconsolata-Regular.ttf")).tweak(
|
||||
FontTweak {
|
||||
scale: 1.22, // This font is smaller than DejaVuSans
|
||||
y_offset_factor: -0.18, // and too low
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: 0.0,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"NotoSansCJK".to_owned(),
|
||||
FontData::from_static(include_bytes!("../assets/fonts/NotoSansCJK-Regular.ttc")),
|
||||
);
|
||||
|
||||
font_data.insert(
|
||||
"NotoSansThai".to_owned(),
|
||||
FontData::from_static(include_bytes!("../assets/fonts/NotoSansThai-Regular.ttf")),
|
||||
);
|
||||
|
||||
// Some good looking emojis. Use as first priority:
|
||||
font_data.insert(
|
||||
"NotoEmoji".to_owned(),
|
||||
FontData::from_static(include_bytes!("../assets/fonts/NotoEmoji-Regular.ttf")).tweak(
|
||||
FontTweak {
|
||||
scale: 1.1, // make them a touch larger
|
||||
y_offset_factor: 0.0,
|
||||
y_offset: 0.0,
|
||||
baseline_offset_factor: 0.0,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
let base_fonts = vec![
|
||||
"DejaVuSans".to_owned(),
|
||||
"NotoEmoji".to_owned(),
|
||||
"NotoSansCJK".to_owned(),
|
||||
"NotoSansThai".to_owned(),
|
||||
];
|
||||
|
||||
let mut proportional = vec!["Onest".to_owned()];
|
||||
proportional.extend(base_fonts.clone());
|
||||
|
||||
let mut medium = vec!["OnestMedium".to_owned()];
|
||||
medium.extend(base_fonts.clone());
|
||||
|
||||
let mut mono = vec!["Inconsolata".to_owned()];
|
||||
mono.extend(base_fonts.clone());
|
||||
|
||||
let mut bold = vec!["OnestBold".to_owned()];
|
||||
bold.extend(base_fonts.clone());
|
||||
|
||||
let emoji = vec!["NotoEmoji".to_owned()];
|
||||
|
||||
families.insert(egui::FontFamily::Proportional, proportional);
|
||||
families.insert(egui::FontFamily::Monospace, mono);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Medium.as_str().into()),
|
||||
medium,
|
||||
);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||
bold,
|
||||
);
|
||||
families.insert(
|
||||
egui::FontFamily::Name(NamedFontFamily::Emoji.as_str().into()),
|
||||
emoji,
|
||||
);
|
||||
|
||||
debug!("fonts: {:?}", families);
|
||||
|
||||
let defs = FontDefinitions {
|
||||
font_data,
|
||||
families,
|
||||
};
|
||||
|
||||
ctx.set_fonts(defs);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/*
|
||||
use egui::util::History;
|
||||
|
||||
pub struct FrameHistory {
|
||||
@@ -46,3 +47,4 @@ impl FrameHistory {
|
||||
egui::warn_if_debug_build(ui);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::error::Error;
|
||||
use crate::imgcache::ImageCache;
|
||||
use crate::result::Result;
|
||||
use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint, TextureHandle};
|
||||
use image::imageops::FilterType;
|
||||
use notedeck::ImageCache;
|
||||
use notedeck::Result;
|
||||
use poll_promise::Promise;
|
||||
use std::path;
|
||||
use tokio::fs;
|
||||
@@ -183,7 +182,7 @@ fn fetch_img_from_disk(
|
||||
let path = path.to_owned();
|
||||
Promise::spawn_async(async move {
|
||||
let data = fs::read(path).await?;
|
||||
let image_buffer = image::load_from_memory(&data)?;
|
||||
let image_buffer = image::load_from_memory(&data).map_err(notedeck::Error::Image)?;
|
||||
|
||||
// TODO: remove unwrap here
|
||||
let flat_samples = image_buffer.as_flat_samples_u8().unwrap();
|
||||
@@ -239,7 +238,7 @@ fn fetch_img_from_net(
|
||||
let cache_path = cache_path.to_owned();
|
||||
ehttp::fetch(request, move |response| {
|
||||
let handle = response
|
||||
.map_err(Error::Generic)
|
||||
.map_err(notedeck::Error::Generic)
|
||||
.and_then(|resp| parse_img_response(resp, imgtyp))
|
||||
.map(|img| {
|
||||
let texture_handle = ctx.load_texture(&cloned_url, img.clone(), Default::default());
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
use crate::Result;
|
||||
use egui::TextureHandle;
|
||||
use poll_promise::Promise;
|
||||
|
||||
use egui::ColorImage;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
|
||||
use std::path;
|
||||
|
||||
pub type ImageCacheValue = Promise<Result<TextureHandle>>;
|
||||
pub type ImageCacheMap = HashMap<String, ImageCacheValue>;
|
||||
|
||||
pub struct ImageCache {
|
||||
pub cache_dir: path::PathBuf,
|
||||
url_imgs: ImageCacheMap,
|
||||
}
|
||||
|
||||
impl ImageCache {
|
||||
pub fn new(cache_dir: path::PathBuf) -> Self {
|
||||
Self {
|
||||
cache_dir,
|
||||
url_imgs: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rel_dir() -> &'static str {
|
||||
"img"
|
||||
}
|
||||
|
||||
pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> {
|
||||
let file_path = cache_dir.join(Self::key(url));
|
||||
let file = File::options()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(file_path)?;
|
||||
let encoder = image::codecs::webp::WebPEncoder::new_lossless(file);
|
||||
|
||||
encoder.encode(
|
||||
data.as_raw(),
|
||||
data.size[0] as u32,
|
||||
data.size[1] as u32,
|
||||
image::ColorType::Rgba8.into(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn key(url: &str) -> String {
|
||||
base32::encode(base32::Alphabet::Crockford, url.as_bytes())
|
||||
}
|
||||
|
||||
pub fn map(&self) -> &ImageCacheMap {
|
||||
&self.url_imgs
|
||||
}
|
||||
|
||||
pub fn map_mut(&mut self) -> &mut ImageCacheMap {
|
||||
&mut self.url_imgs
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ mod abbrev;
|
||||
pub mod accounts;
|
||||
mod actionbar;
|
||||
pub mod app_creation;
|
||||
mod app_size_handler;
|
||||
mod app_style;
|
||||
mod args;
|
||||
mod colors;
|
||||
@@ -15,35 +14,24 @@ mod column;
|
||||
mod deck_state;
|
||||
mod decks;
|
||||
mod draft;
|
||||
mod filter;
|
||||
mod fonts;
|
||||
mod frame_history;
|
||||
mod images;
|
||||
mod imgcache;
|
||||
mod key_parsing;
|
||||
pub mod login_manager;
|
||||
mod multi_subscriber;
|
||||
mod muted;
|
||||
mod nav;
|
||||
mod note;
|
||||
mod notecache;
|
||||
mod notes_holder;
|
||||
mod post;
|
||||
mod profile;
|
||||
pub mod relay_pool_manager;
|
||||
mod result;
|
||||
mod route;
|
||||
mod subscriptions;
|
||||
mod support;
|
||||
mod test_data;
|
||||
mod theme_handler;
|
||||
mod thread;
|
||||
mod time;
|
||||
mod timecache;
|
||||
mod timeline;
|
||||
pub mod ui;
|
||||
mod unknowns;
|
||||
mod user_account;
|
||||
mod view_state;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3,7 +3,8 @@ use nostrdb::{Ndb, Note, Transaction};
|
||||
use tracing::{debug, error, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{filter::UnifiedSubscription, muted::MuteFun, note::NoteRef, Error};
|
||||
use crate::Error;
|
||||
use notedeck::{MuteFun, NoteRef, UnifiedSubscription};
|
||||
|
||||
pub struct MultiSubscriber {
|
||||
filters: Vec<Filter>,
|
||||
@@ -111,7 +112,7 @@ impl MultiSubscriber {
|
||||
txn: &Transaction,
|
||||
is_muted: &MuteFun,
|
||||
) -> Result<Vec<NoteRef>, Error> {
|
||||
let sub = self.sub.as_ref().ok_or(Error::no_active_sub())?;
|
||||
let sub = self.sub.as_ref().ok_or(notedeck::Error::no_active_sub())?;
|
||||
let new_note_keys = ndb.poll_for_notes(sub.local, 500);
|
||||
|
||||
if new_note_keys.is_empty() {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
use nostrdb::Note;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use tracing::debug;
|
||||
|
||||
pub type MuteFun = dyn Fn(&Note) -> bool;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Muted {
|
||||
// TODO - implement private mutes
|
||||
pub pubkeys: BTreeSet<[u8; 32]>,
|
||||
pub hashtags: BTreeSet<String>,
|
||||
pub words: BTreeSet<String>,
|
||||
pub threads: BTreeSet<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Muted {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Muted")
|
||||
.field(
|
||||
"pubkeys",
|
||||
&self.pubkeys.iter().map(hex::encode).collect::<Vec<_>>(),
|
||||
)
|
||||
.field("hashtags", &self.hashtags)
|
||||
.field("words", &self.words)
|
||||
.field(
|
||||
"threads",
|
||||
&self.threads.iter().map(hex::encode).collect::<Vec<_>>(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Muted {
|
||||
pub fn is_muted(&self, note: &Note) -> bool {
|
||||
if self.pubkeys.contains(note.pubkey()) {
|
||||
debug!(
|
||||
"{}: MUTED pubkey: {}",
|
||||
hex::encode(note.id()),
|
||||
hex::encode(note.pubkey())
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// FIXME - Implement hashtag muting here
|
||||
|
||||
// TODO - let's not add this for now, we will likely need to
|
||||
// have an optimized data structure in nostrdb to properly
|
||||
// mute words. this mutes substrings which is not ideal.
|
||||
//
|
||||
// let content = note.content().to_lowercase();
|
||||
// for word in &self.words {
|
||||
// if content.contains(&word.to_lowercase()) {
|
||||
// debug!("{}: MUTED word: {}", hex::encode(note.id()), word);
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
// FIXME - Implement thread muting here
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
accounts::{render_accounts_route, AccountsAction},
|
||||
accounts::render_accounts_route,
|
||||
actionbar::NoteAction,
|
||||
app::{get_active_columns, get_active_columns_mut, get_decks_mut},
|
||||
column::ColumnsAction,
|
||||
@@ -27,6 +27,8 @@ use crate::{
|
||||
Damus,
|
||||
};
|
||||
|
||||
use notedeck::{AccountsAction, AppContext};
|
||||
|
||||
use egui_nav::{Nav, NavAction, NavResponse, NavUiType};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use tracing::{error, info};
|
||||
@@ -48,23 +50,23 @@ pub enum SwitchingAction {
|
||||
|
||||
impl SwitchingAction {
|
||||
/// process the action, and return whether switching occured
|
||||
pub fn process(&self, app: &mut Damus) -> bool {
|
||||
pub fn process(&self, app: &mut Damus, ctx: &mut AppContext<'_>) -> bool {
|
||||
match &self {
|
||||
SwitchingAction::Accounts(account_action) => match *account_action {
|
||||
AccountsAction::Switch(index) => app.accounts.select_account(index),
|
||||
AccountsAction::Remove(index) => app.accounts.remove_account(index),
|
||||
AccountsAction::Switch(index) => ctx.accounts.select_account(index),
|
||||
AccountsAction::Remove(index) => ctx.accounts.remove_account(index),
|
||||
},
|
||||
SwitchingAction::Columns(columns_action) => match *columns_action {
|
||||
ColumnsAction::Remove(index) => {
|
||||
get_active_columns_mut(&app.accounts, &mut app.decks_cache).delete_column(index)
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache).delete_column(index)
|
||||
}
|
||||
},
|
||||
SwitchingAction::Decks(decks_action) => match *decks_action {
|
||||
DecksAction::Switch(index) => {
|
||||
get_decks_mut(&app.accounts, &mut app.decks_cache).set_active(index)
|
||||
get_decks_mut(ctx.accounts, &mut app.decks_cache).set_active(index)
|
||||
}
|
||||
DecksAction::Removing(index) => {
|
||||
get_decks_mut(&app.accounts, &mut app.decks_cache).remove_deck(index)
|
||||
get_decks_mut(ctx.accounts, &mut app.decks_cache).remove_deck(index)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -98,7 +100,7 @@ impl RenderNavResponse {
|
||||
}
|
||||
|
||||
#[must_use = "Make sure to save columns if result is true"]
|
||||
pub fn process_render_nav_response(&self, app: &mut Damus) -> bool {
|
||||
pub fn process_render_nav_response(&self, app: &mut Damus, ctx: &mut AppContext<'_>) -> bool {
|
||||
let mut switching_occured: bool = false;
|
||||
let col = self.column;
|
||||
|
||||
@@ -111,46 +113,51 @@ impl RenderNavResponse {
|
||||
// start returning when we're finished posting
|
||||
match action {
|
||||
RenderNavAction::Back => {
|
||||
app.columns_mut().column_mut(col).router_mut().go_back();
|
||||
app.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.go_back();
|
||||
}
|
||||
|
||||
RenderNavAction::RemoveColumn => {
|
||||
let tl = app.columns().find_timeline_for_column_index(col);
|
||||
let tl = app
|
||||
.columns(ctx.accounts)
|
||||
.find_timeline_for_column_index(col);
|
||||
if let Some(timeline) = tl {
|
||||
unsubscribe_timeline(app.ndb(), timeline);
|
||||
unsubscribe_timeline(ctx.ndb, timeline);
|
||||
}
|
||||
|
||||
app.columns_mut().delete_column(col);
|
||||
app.columns_mut(ctx.accounts).delete_column(col);
|
||||
switching_occured = true;
|
||||
}
|
||||
|
||||
RenderNavAction::PostAction(post_action) => {
|
||||
let txn = Transaction::new(&app.ndb).expect("txn");
|
||||
let _ = post_action.execute(&app.ndb, &txn, &mut app.pool, &mut app.drafts);
|
||||
get_active_columns_mut(&app.accounts, &mut app.decks_cache)
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
let _ = post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts);
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.go_back();
|
||||
}
|
||||
|
||||
RenderNavAction::NoteAction(note_action) => {
|
||||
let txn = Transaction::new(&app.ndb).expect("txn");
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
|
||||
note_action.execute_and_process_result(
|
||||
&app.ndb,
|
||||
get_active_columns_mut(&app.accounts, &mut app.decks_cache),
|
||||
ctx.ndb,
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
|
||||
col,
|
||||
&mut app.threads,
|
||||
&mut app.profiles,
|
||||
&mut app.note_cache,
|
||||
&mut app.pool,
|
||||
ctx.note_cache,
|
||||
ctx.pool,
|
||||
&txn,
|
||||
&app.accounts.mutefun(),
|
||||
&ctx.accounts.mutefun(),
|
||||
);
|
||||
}
|
||||
|
||||
RenderNavAction::SwitchingAction(switching_action) => {
|
||||
switching_occured = switching_action.process(app);
|
||||
switching_occured = switching_action.process(app, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,37 +165,41 @@ impl RenderNavResponse {
|
||||
if let Some(action) = self.response.action {
|
||||
match action {
|
||||
NavAction::Returned => {
|
||||
let r = app.columns_mut().column_mut(col).router_mut().pop();
|
||||
let txn = Transaction::new(&app.ndb).expect("txn");
|
||||
let r = app
|
||||
.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.pop();
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r {
|
||||
let root_id = {
|
||||
crate::note::root_note_id_from_selected_id(
|
||||
&app.ndb,
|
||||
&mut app.note_cache,
|
||||
notedeck::note::root_note_id_from_selected_id(
|
||||
ctx.ndb,
|
||||
ctx.note_cache,
|
||||
&txn,
|
||||
id.bytes(),
|
||||
)
|
||||
};
|
||||
Thread::unsubscribe_locally(
|
||||
&txn,
|
||||
&app.ndb,
|
||||
&mut app.note_cache,
|
||||
ctx.ndb,
|
||||
ctx.note_cache,
|
||||
&mut app.threads,
|
||||
&mut app.pool,
|
||||
ctx.pool,
|
||||
root_id,
|
||||
&app.accounts.mutefun(),
|
||||
&ctx.accounts.mutefun(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r {
|
||||
Profile::unsubscribe_locally(
|
||||
&txn,
|
||||
&app.ndb,
|
||||
&mut app.note_cache,
|
||||
ctx.ndb,
|
||||
ctx.note_cache,
|
||||
&mut app.profiles,
|
||||
&mut app.pool,
|
||||
ctx.pool,
|
||||
pubkey.bytes(),
|
||||
&app.accounts.mutefun(),
|
||||
&ctx.accounts.mutefun(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,7 +207,7 @@ impl RenderNavResponse {
|
||||
}
|
||||
|
||||
NavAction::Navigated => {
|
||||
let cur_router = app.columns_mut().column_mut(col).router_mut();
|
||||
let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut();
|
||||
cur_router.navigating = false;
|
||||
if cur_router.is_replacing() {
|
||||
cur_router.remove_previous_routes();
|
||||
@@ -218,20 +229,21 @@ impl RenderNavResponse {
|
||||
fn render_nav_body(
|
||||
ui: &mut egui::Ui,
|
||||
app: &mut Damus,
|
||||
ctx: &mut AppContext<'_>,
|
||||
top: &Route,
|
||||
col: usize,
|
||||
) -> Option<RenderNavAction> {
|
||||
match top {
|
||||
Route::Timeline(tlr) => render_timeline_route(
|
||||
&app.ndb,
|
||||
get_active_columns_mut(&app.accounts, &mut app.decks_cache),
|
||||
ctx.ndb,
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
|
||||
&mut app.drafts,
|
||||
&mut app.img_cache,
|
||||
&mut app.unknown_ids,
|
||||
&mut app.note_cache,
|
||||
ctx.img_cache,
|
||||
ctx.unknown_ids,
|
||||
ctx.note_cache,
|
||||
&mut app.threads,
|
||||
&mut app.profiles,
|
||||
&mut app.accounts,
|
||||
ctx.accounts,
|
||||
*tlr,
|
||||
col,
|
||||
app.textmode,
|
||||
@@ -240,36 +252,36 @@ fn render_nav_body(
|
||||
Route::Accounts(amr) => {
|
||||
let mut action = render_accounts_route(
|
||||
ui,
|
||||
&app.ndb,
|
||||
ctx.ndb,
|
||||
col,
|
||||
&mut app.img_cache,
|
||||
&mut app.accounts,
|
||||
ctx.img_cache,
|
||||
ctx.accounts,
|
||||
&mut app.decks_cache,
|
||||
&mut app.view_state.login,
|
||||
*amr,
|
||||
);
|
||||
let txn = Transaction::new(&app.ndb).expect("txn");
|
||||
action.process_action(&mut app.unknown_ids, &app.ndb, &txn);
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
|
||||
action
|
||||
.accounts_action
|
||||
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
|
||||
}
|
||||
Route::Relays => {
|
||||
let manager = RelayPoolManager::new(app.pool_mut());
|
||||
let manager = RelayPoolManager::new(ctx.pool);
|
||||
RelayView::new(manager).ui(ui);
|
||||
None
|
||||
}
|
||||
Route::ComposeNote => {
|
||||
let kp = app.accounts.get_selected_account()?.to_full()?;
|
||||
let kp = ctx.accounts.get_selected_account()?.to_full()?;
|
||||
let draft = app.drafts.compose_mut();
|
||||
|
||||
let txn = Transaction::new(&app.ndb).expect("txn");
|
||||
let txn = Transaction::new(ctx.ndb).expect("txn");
|
||||
let post_response = ui::PostView::new(
|
||||
&app.ndb,
|
||||
ctx.ndb,
|
||||
draft,
|
||||
PostType::New,
|
||||
&mut app.img_cache,
|
||||
&mut app.note_cache,
|
||||
ctx.img_cache,
|
||||
ctx.note_cache,
|
||||
kp,
|
||||
)
|
||||
.ui(&txn, ui);
|
||||
@@ -277,7 +289,7 @@ fn render_nav_body(
|
||||
post_response.action.map(Into::into)
|
||||
}
|
||||
Route::AddColumn(route) => {
|
||||
render_add_column_routes(ui, app, col, route);
|
||||
render_add_column_routes(ui, app, ctx, col, route);
|
||||
|
||||
None
|
||||
}
|
||||
@@ -290,14 +302,14 @@ fn render_nav_body(
|
||||
let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
|
||||
let mut resp = None;
|
||||
if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) {
|
||||
if let Some(cur_acc) = app.accounts.get_selected_account() {
|
||||
if let Some(cur_acc) = ctx.accounts.get_selected_account() {
|
||||
app.decks_cache.add_deck(
|
||||
cur_acc.pubkey,
|
||||
Deck::new(config_resp.icon, config_resp.name),
|
||||
);
|
||||
|
||||
// set new deck as active
|
||||
let cur_index = get_decks_mut(&app.accounts, &mut app.decks_cache)
|
||||
let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache)
|
||||
.decks()
|
||||
.len()
|
||||
- 1;
|
||||
@@ -307,7 +319,7 @@ fn render_nav_body(
|
||||
}
|
||||
|
||||
new_deck_state.clear();
|
||||
get_active_columns_mut(&app.accounts, &mut app.decks_cache)
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
|
||||
.get_first_router()
|
||||
.go_back();
|
||||
}
|
||||
@@ -315,13 +327,13 @@ fn render_nav_body(
|
||||
}
|
||||
Route::EditDeck(index) => {
|
||||
let mut action = None;
|
||||
let cur_deck = get_decks_mut(&app.accounts, &mut app.decks_cache)
|
||||
let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache)
|
||||
.decks_mut()
|
||||
.get_mut(*index)
|
||||
.expect("index wasn't valid");
|
||||
let id = ui.id().with((
|
||||
"edit-deck",
|
||||
app.accounts.get_selected_account().map(|k| k.pubkey),
|
||||
ctx.accounts.get_selected_account().map(|k| k.pubkey),
|
||||
index,
|
||||
));
|
||||
let deck_state = app
|
||||
@@ -340,7 +352,7 @@ fn render_nav_body(
|
||||
)));
|
||||
}
|
||||
}
|
||||
get_active_columns_mut(&app.accounts, &mut app.decks_cache)
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
|
||||
.get_first_router()
|
||||
.go_back();
|
||||
}
|
||||
@@ -351,25 +363,46 @@ fn render_nav_body(
|
||||
}
|
||||
|
||||
#[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
|
||||
pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> RenderNavResponse {
|
||||
let col_id = get_active_columns(&app.accounts, &app.decks_cache).get_column_id_at_index(col);
|
||||
pub fn render_nav(
|
||||
col: usize,
|
||||
app: &mut Damus,
|
||||
ctx: &mut AppContext<'_>,
|
||||
ui: &mut egui::Ui,
|
||||
) -> RenderNavResponse {
|
||||
let col_id = get_active_columns(ctx.accounts, &app.decks_cache).get_column_id_at_index(col);
|
||||
// TODO(jb55): clean up this router_mut mess by using Router<R> in egui-nav directly
|
||||
|
||||
let nav_response = Nav::new(&app.columns().column(col).router().routes().clone())
|
||||
.navigating(app.columns_mut().column_mut(col).router_mut().navigating)
|
||||
.returning(app.columns_mut().column_mut(col).router_mut().returning)
|
||||
.id_source(egui::Id::new(col_id))
|
||||
.show_mut(ui, |ui, render_type, nav| match render_type {
|
||||
NavUiType::Title => NavTitle::new(
|
||||
&app.ndb,
|
||||
&mut app.img_cache,
|
||||
get_active_columns_mut(&app.accounts, &mut app.decks_cache),
|
||||
app.accounts.get_selected_account().map(|a| &a.pubkey),
|
||||
nav.routes(),
|
||||
)
|
||||
.show(ui),
|
||||
NavUiType::Body => render_nav_body(ui, app, nav.routes().last().expect("top"), col),
|
||||
});
|
||||
let nav_response = Nav::new(
|
||||
&app.columns(ctx.accounts)
|
||||
.column(col)
|
||||
.router()
|
||||
.routes()
|
||||
.clone(),
|
||||
)
|
||||
.navigating(
|
||||
app.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.navigating,
|
||||
)
|
||||
.returning(
|
||||
app.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.returning,
|
||||
)
|
||||
.id_source(egui::Id::new(col_id))
|
||||
.show_mut(ui, |ui, render_type, nav| match render_type {
|
||||
NavUiType::Title => NavTitle::new(
|
||||
ctx.ndb,
|
||||
ctx.img_cache,
|
||||
get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
|
||||
ctx.accounts.get_selected_account().map(|a| &a.pubkey),
|
||||
nav.routes(),
|
||||
)
|
||||
.show(ui),
|
||||
NavUiType::Body => render_nav_body(ui, app, ctx, nav.routes().last().expect("top"), col),
|
||||
});
|
||||
|
||||
RenderNavResponse::new(col, nav_response)
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
use crate::notecache::NoteCache;
|
||||
use nostrdb::{Ndb, Note, NoteKey, QueryResult, Transaction};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub struct NoteRef {
|
||||
pub key: NoteKey,
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
impl NoteRef {
|
||||
pub fn new(key: NoteKey, created_at: u64) -> Self {
|
||||
NoteRef { key, created_at }
|
||||
}
|
||||
|
||||
pub fn from_note(note: &Note<'_>) -> Self {
|
||||
let created_at = note.created_at();
|
||||
let key = note.key().expect("todo: implement NoteBuf");
|
||||
NoteRef::new(key, created_at)
|
||||
}
|
||||
|
||||
pub fn from_query_result(qr: QueryResult<'_>) -> Self {
|
||||
NoteRef {
|
||||
key: qr.note_key,
|
||||
created_at: qr.note.created_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for NoteRef {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match self.created_at.cmp(&other.created_at) {
|
||||
Ordering::Equal => self.key.cmp(&other.key),
|
||||
Ordering::Less => Ordering::Greater,
|
||||
Ordering::Greater => Ordering::Less,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for NoteRef {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root_note_id_from_selected_id<'a>(
|
||||
ndb: &Ndb,
|
||||
note_cache: &mut NoteCache,
|
||||
txn: &'a Transaction,
|
||||
selected_note_id: &'a [u8; 32],
|
||||
) -> &'a [u8; 32] {
|
||||
let selected_note_key = if let Ok(key) = ndb
|
||||
.get_notekey_by_id(txn, selected_note_id)
|
||||
.map(NoteKey::new)
|
||||
{
|
||||
key
|
||||
} else {
|
||||
return selected_note_id;
|
||||
};
|
||||
|
||||
let note = if let Ok(note) = ndb.get_note_by_key(txn, selected_note_key) {
|
||||
note
|
||||
} else {
|
||||
return selected_note_id;
|
||||
};
|
||||
|
||||
note_cache
|
||||
.cached_note_or_insert(selected_note_key, ¬e)
|
||||
.reply
|
||||
.borrow(note.tags())
|
||||
.root()
|
||||
.map_or_else(|| selected_note_id, |nr| nr.id)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
use crate::time::time_ago_since;
|
||||
use crate::timecache::TimeCached;
|
||||
use nostrdb::{Note, NoteKey, NoteReply, NoteReplyBuf};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NoteCache {
|
||||
pub cache: HashMap<NoteKey, CachedNote>,
|
||||
}
|
||||
|
||||
impl NoteCache {
|
||||
pub fn cached_note_or_insert_mut(&mut self, note_key: NoteKey, note: &Note) -> &mut CachedNote {
|
||||
self.cache
|
||||
.entry(note_key)
|
||||
.or_insert_with(|| CachedNote::new(note))
|
||||
}
|
||||
|
||||
pub fn cached_note(&self, note_key: NoteKey) -> Option<&CachedNote> {
|
||||
self.cache.get(¬e_key)
|
||||
}
|
||||
|
||||
pub fn cache_mut(&mut self) -> &mut HashMap<NoteKey, CachedNote> {
|
||||
&mut self.cache
|
||||
}
|
||||
|
||||
pub fn cached_note_or_insert(&mut self, note_key: NoteKey, note: &Note) -> &CachedNote {
|
||||
self.cache
|
||||
.entry(note_key)
|
||||
.or_insert_with(|| CachedNote::new(note))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CachedNote {
|
||||
reltime: TimeCached<String>,
|
||||
pub reply: NoteReplyBuf,
|
||||
}
|
||||
|
||||
impl CachedNote {
|
||||
pub fn new(note: &Note<'_>) -> Self {
|
||||
let created_at = note.created_at();
|
||||
let reltime = TimeCached::new(
|
||||
Duration::from_secs(1),
|
||||
Box::new(move || time_ago_since(created_at)),
|
||||
);
|
||||
let reply = NoteReply::new(note.tags()).to_owned();
|
||||
CachedNote { reltime, reply }
|
||||
}
|
||||
|
||||
pub fn reltime_str_mut(&mut self) -> &str {
|
||||
self.reltime.get_mut()
|
||||
}
|
||||
|
||||
pub fn reltime_str(&self) -> Option<&str> {
|
||||
self.reltime.get().map(|x| x.as_str())
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@ use std::collections::HashMap;
|
||||
|
||||
use enostr::{Filter, RelayPool};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{MuteFun, NoteCache, NoteRef, NoteRefsUnkIdAction};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{
|
||||
actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, muted::MuteFun, note::NoteRef,
|
||||
notecache::NoteCache, timeline::TimelineTab, unknowns::NoteRefsUnkIdAction, Error, Result,
|
||||
actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, timeline::TimelineTab, Error,
|
||||
Result,
|
||||
};
|
||||
|
||||
pub struct NotesHolderStorage<M: NotesHolder> {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use enostr::{Filter, Pubkey};
|
||||
use nostrdb::{FilterBuilder, Ndb, ProfileRecord, Transaction};
|
||||
|
||||
use notedeck::{filter::default_limit, FilterState, MuteFun, NoteCache, NoteRef};
|
||||
|
||||
use crate::{
|
||||
filter::{self, FilterState},
|
||||
multi_subscriber::MultiSubscriber,
|
||||
muted::MuteFun,
|
||||
note::NoteRef,
|
||||
notecache::NoteCache,
|
||||
notes_holder::NotesHolder,
|
||||
timeline::{copy_notes_into_timeline, PubkeySource, Timeline, TimelineKind},
|
||||
};
|
||||
@@ -79,7 +77,7 @@ impl Profile {
|
||||
vec![Filter::new()
|
||||
.authors([pk])
|
||||
.kinds([1])
|
||||
.limit(filter::default_limit())]
|
||||
.limit(default_limit())]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
use crate::error::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -15,7 +15,7 @@ use crate::{
|
||||
Error,
|
||||
};
|
||||
|
||||
use super::{write_file, DataPath, DataPathType, Directory};
|
||||
use notedeck::{storage, DataPath, DataPathType, Directory};
|
||||
|
||||
pub static DECKS_CACHE_FILE: &str = "decks_cache.json";
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn save_decks_cache(path: &DataPath, decks_cache: &DecksCache) {
|
||||
|
||||
let data_path = path.path(DataPathType::Setting);
|
||||
|
||||
if let Err(e) = write_file(
|
||||
if let Err(e) = storage::write_file(
|
||||
&data_path,
|
||||
DECKS_CACHE_FILE.to_string(),
|
||||
&serialized_decks_cache,
|
||||
@@ -761,12 +761,13 @@ impl fmt::Display for Selection {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enostr::Pubkey;
|
||||
//use enostr::Pubkey;
|
||||
|
||||
use crate::{route::Route, test_data::test_app, timeline::TimelineRoute};
|
||||
//use crate::{route::Route, timeline::TimelineRoute};
|
||||
|
||||
use super::deserialize_columns;
|
||||
//use super::deserialize_columns;
|
||||
|
||||
/* TODO: re-enable once we have test_app working again
|
||||
#[test]
|
||||
fn test_deserialize_columns() {
|
||||
let serialized = vec![
|
||||
@@ -800,4 +801,5 @@ mod tests {
|
||||
panic!("The second router route is not a TimelineRoute::Timeline variant");
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
use eframe::Result;
|
||||
use enostr::{Keypair, Pubkey, SerializableKeypair};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
use super::{
|
||||
file_storage::{delete_file, write_file, Directory},
|
||||
key_storage_impl::{KeyStorageError, KeyStorageResponse},
|
||||
};
|
||||
|
||||
static SELECTED_PUBKEY_FILE_NAME: &str = "selected_pubkey";
|
||||
|
||||
/// An OS agnostic file key storage implementation
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct FileKeyStorage {
|
||||
keys_directory: Directory,
|
||||
selected_key_directory: Directory,
|
||||
}
|
||||
|
||||
impl FileKeyStorage {
|
||||
pub fn new(keys_directory: Directory, selected_key_directory: Directory) -> Self {
|
||||
Self {
|
||||
keys_directory,
|
||||
selected_key_directory,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
||||
write_file(
|
||||
&self.keys_directory.file_path,
|
||||
key.pubkey.hex(),
|
||||
&serde_json::to_string(&SerializableKeypair::from_keypair(key, "", 7))
|
||||
.map_err(|e| KeyStorageError::Addition(Error::Generic(e.to_string())))?,
|
||||
)
|
||||
.map_err(KeyStorageError::Addition)
|
||||
}
|
||||
|
||||
fn get_keys_internal(&self) -> Result<Vec<Keypair>, KeyStorageError> {
|
||||
let keys = self
|
||||
.keys_directory
|
||||
.get_files()
|
||||
.map_err(KeyStorageError::Retrieval)?
|
||||
.values()
|
||||
.filter_map(|str_key| serde_json::from_str::<SerializableKeypair>(str_key).ok())
|
||||
.map(|serializable_keypair| serializable_keypair.to_keypair(""))
|
||||
.collect();
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
fn remove_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
||||
delete_file(&self.keys_directory.file_path, key.pubkey.hex())
|
||||
.map_err(KeyStorageError::Removal)
|
||||
}
|
||||
|
||||
fn get_selected_pubkey(&self) -> Result<Option<Pubkey>, KeyStorageError> {
|
||||
let pubkey_str = self
|
||||
.selected_key_directory
|
||||
.get_file(SELECTED_PUBKEY_FILE_NAME.to_owned())
|
||||
.map_err(KeyStorageError::Selection)?;
|
||||
|
||||
serde_json::from_str(&pubkey_str)
|
||||
.map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string())))
|
||||
}
|
||||
|
||||
fn select_pubkey(&self, pubkey: Option<Pubkey>) -> Result<(), KeyStorageError> {
|
||||
if let Some(pubkey) = pubkey {
|
||||
write_file(
|
||||
&self.selected_key_directory.file_path,
|
||||
SELECTED_PUBKEY_FILE_NAME.to_owned(),
|
||||
&serde_json::to_string(&pubkey.hex())
|
||||
.map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string())))?,
|
||||
)
|
||||
.map_err(KeyStorageError::Selection)
|
||||
} else if self
|
||||
.selected_key_directory
|
||||
.get_file(SELECTED_PUBKEY_FILE_NAME.to_owned())
|
||||
.is_ok()
|
||||
{
|
||||
// Case where user chose to have no selected pubkey, but one already exists
|
||||
delete_file(
|
||||
&self.selected_key_directory.file_path,
|
||||
SELECTED_PUBKEY_FILE_NAME.to_owned(),
|
||||
)
|
||||
.map_err(KeyStorageError::Selection)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileKeyStorage {
|
||||
pub fn get_keys(&self) -> KeyStorageResponse<Vec<enostr::Keypair>> {
|
||||
KeyStorageResponse::ReceivedResult(self.get_keys_internal())
|
||||
}
|
||||
|
||||
pub fn add_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
|
||||
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
|
||||
}
|
||||
|
||||
pub fn remove_key(&self, key: &enostr::Keypair) -> KeyStorageResponse<()> {
|
||||
KeyStorageResponse::ReceivedResult(self.remove_key_internal(key))
|
||||
}
|
||||
|
||||
pub fn get_selected_key(&self) -> KeyStorageResponse<Option<Pubkey>> {
|
||||
KeyStorageResponse::ReceivedResult(self.get_selected_pubkey())
|
||||
}
|
||||
|
||||
pub fn select_key(&self, key: Option<Pubkey>) -> KeyStorageResponse<()> {
|
||||
KeyStorageResponse::ReceivedResult(self.select_pubkey(key))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
use enostr::Keypair;
|
||||
static CREATE_TMP_DIR: fn() -> Result<PathBuf, Error> =
|
||||
|| Ok(tempfile::TempDir::new()?.path().to_path_buf());
|
||||
|
||||
impl FileKeyStorage {
|
||||
fn mock() -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
keys_directory: Directory::new(CREATE_TMP_DIR()?),
|
||||
selected_key_directory: Directory::new(CREATE_TMP_DIR()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
let kp = enostr::FullKeypair::generate().to_keypair();
|
||||
let storage = FileKeyStorage::mock().unwrap();
|
||||
let resp = storage.add_key(&kp);
|
||||
|
||||
assert_eq!(resp, KeyStorageResponse::ReceivedResult(Ok(())));
|
||||
assert_num_storage(&storage.get_keys(), 1);
|
||||
|
||||
assert_eq!(
|
||||
storage.remove_key(&kp),
|
||||
KeyStorageResponse::ReceivedResult(Ok(()))
|
||||
);
|
||||
assert_num_storage(&storage.get_keys(), 0);
|
||||
}
|
||||
|
||||
fn assert_num_storage(keys_response: &KeyStorageResponse<Vec<Keypair>>, n: usize) {
|
||||
match keys_response {
|
||||
KeyStorageResponse::ReceivedResult(Ok(keys)) => {
|
||||
assert_eq!(keys.len(), n);
|
||||
}
|
||||
KeyStorageResponse::ReceivedResult(Err(_e)) => {
|
||||
panic!("could not get keys");
|
||||
}
|
||||
KeyStorageResponse::Waiting => {
|
||||
panic!("did not receive result");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_key() {
|
||||
let kp = enostr::FullKeypair::generate().to_keypair();
|
||||
|
||||
let storage = FileKeyStorage::mock().unwrap();
|
||||
let _ = storage.add_key(&kp);
|
||||
assert_num_storage(&storage.get_keys(), 1);
|
||||
|
||||
let resp = storage.select_pubkey(Some(kp.pubkey));
|
||||
assert!(resp.is_ok());
|
||||
|
||||
let resp = storage.get_selected_pubkey();
|
||||
|
||||
assert!(resp.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
fs::{self, File},
|
||||
io::{self, BufRead},
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataPath {
|
||||
base: PathBuf,
|
||||
}
|
||||
|
||||
impl DataPath {
|
||||
pub fn new(base: impl AsRef<Path>) -> Self {
|
||||
let base = base.as_ref().to_path_buf();
|
||||
Self { base }
|
||||
}
|
||||
|
||||
pub fn default_base() -> Option<PathBuf> {
|
||||
dirs::data_local_dir().map(|pb| pb.join("notedeck"))
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DataPathType {
|
||||
Log,
|
||||
Setting,
|
||||
Keys,
|
||||
SelectedKey,
|
||||
Db,
|
||||
Cache,
|
||||
}
|
||||
|
||||
impl DataPath {
|
||||
pub fn rel_path(&self, typ: DataPathType) -> PathBuf {
|
||||
match typ {
|
||||
DataPathType::Log => PathBuf::from("logs"),
|
||||
DataPathType::Setting => PathBuf::from("settings"),
|
||||
DataPathType::Keys => PathBuf::from("storage").join("accounts"),
|
||||
DataPathType::SelectedKey => PathBuf::from("storage").join("selected_account"),
|
||||
DataPathType::Db => PathBuf::from("db"),
|
||||
DataPathType::Cache => PathBuf::from("cache"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self, typ: DataPathType) -> PathBuf {
|
||||
self.base.join(self.rel_path(typ))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Directory {
|
||||
pub file_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
pub fn new(file_path: PathBuf) -> Self {
|
||||
Self { file_path }
|
||||
}
|
||||
|
||||
/// Get the files in the current directory where the key is the file name and the value is the file contents
|
||||
pub fn get_files(&self) -> Result<HashMap<String, String>, Error> {
|
||||
let dir = fs::read_dir(self.file_path.clone())?;
|
||||
let map = dir
|
||||
.filter_map(|f| f.ok())
|
||||
.filter(|f| f.path().is_file())
|
||||
.filter_map(|f| {
|
||||
let file_name = f.file_name().into_string().ok()?;
|
||||
let contents = fs::read_to_string(f.path()).ok()?;
|
||||
Some((file_name, contents))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
pub fn get_file_names(&self) -> Result<Vec<String>, Error> {
|
||||
let dir = fs::read_dir(self.file_path.clone())?;
|
||||
let names = dir
|
||||
.filter_map(|f| f.ok())
|
||||
.filter(|f| f.path().is_file())
|
||||
.filter_map(|f| f.file_name().into_string().ok())
|
||||
.collect();
|
||||
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
pub fn get_file(&self, file_name: String) -> Result<String, Error> {
|
||||
let filepath = self.file_path.clone().join(file_name.clone());
|
||||
|
||||
if filepath.exists() && filepath.is_file() {
|
||||
let filepath_str = filepath
|
||||
.to_str()
|
||||
.ok_or_else(|| Error::Generic("Could not turn path to string".to_owned()))?;
|
||||
Ok(fs::read_to_string(filepath_str)?)
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"Requested file was not found: {}",
|
||||
file_name
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_file_last_n_lines(&self, file_name: String, n: usize) -> Result<FileResult, Error> {
|
||||
let filepath = self.file_path.clone().join(file_name.clone());
|
||||
|
||||
if filepath.exists() && filepath.is_file() {
|
||||
let file = File::open(&filepath)?;
|
||||
let reader = io::BufReader::new(file);
|
||||
|
||||
let mut queue: VecDeque<String> = VecDeque::with_capacity(n);
|
||||
|
||||
let mut total_lines_in_file = 0;
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
|
||||
queue.push_back(line);
|
||||
|
||||
if queue.len() > n {
|
||||
queue.pop_front();
|
||||
}
|
||||
total_lines_in_file += 1;
|
||||
}
|
||||
|
||||
let output_num_lines = queue.len();
|
||||
let output = queue.into_iter().collect::<Vec<String>>().join("\n");
|
||||
Ok(FileResult {
|
||||
output,
|
||||
output_num_lines,
|
||||
total_lines_in_file,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"Requested file was not found: {}",
|
||||
file_name
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the file name which is most recently modified in the directory
|
||||
pub fn get_most_recent(&self) -> Result<Option<String>, Error> {
|
||||
let mut most_recent: Option<(SystemTime, String)> = None;
|
||||
|
||||
for entry in fs::read_dir(&self.file_path)? {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
if metadata.is_file() {
|
||||
let modified = metadata.modified()?;
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
match most_recent {
|
||||
Some((last_modified, _)) if modified > last_modified => {
|
||||
most_recent = Some((modified, file_name));
|
||||
}
|
||||
None => {
|
||||
most_recent = Some((modified, file_name));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(most_recent.map(|(_, file_name)| file_name))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileResult {
|
||||
pub output: String,
|
||||
pub output_num_lines: usize,
|
||||
pub total_lines_in_file: usize,
|
||||
}
|
||||
|
||||
/// Write the file to the directory
|
||||
pub fn write_file(directory: &Path, file_name: String, data: &str) -> Result<(), Error> {
|
||||
if !directory.exists() {
|
||||
fs::create_dir_all(directory)?
|
||||
}
|
||||
|
||||
std::fs::write(directory.join(file_name), data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_file(directory: &Path, file_name: String) -> Result<(), Error> {
|
||||
let file_to_delete = directory.join(file_name.clone());
|
||||
if file_to_delete.exists() && file_to_delete.is_file() {
|
||||
fs::remove_file(file_to_delete).map_err(Error::Io)
|
||||
} else {
|
||||
Err(Error::Generic(format!(
|
||||
"Requested file to delete was not found: {}",
|
||||
file_name
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
storage::file_storage::{delete_file, write_file},
|
||||
Error,
|
||||
};
|
||||
|
||||
use super::Directory;
|
||||
|
||||
static CREATE_TMP_DIR: fn() -> Result<PathBuf, Error> =
|
||||
|| Ok(tempfile::TempDir::new()?.path().to_path_buf());
|
||||
|
||||
#[test]
|
||||
fn test_add_get_delete() {
|
||||
if let Ok(path) = CREATE_TMP_DIR() {
|
||||
let directory = Directory::new(path);
|
||||
let file_name = "file_test_name.txt".to_string();
|
||||
let file_contents = "test";
|
||||
let write_res = write_file(&directory.file_path, file_name.clone(), file_contents);
|
||||
assert!(write_res.is_ok());
|
||||
|
||||
if let Ok(asserted_file_contents) = directory.get_file(file_name.clone()) {
|
||||
assert_eq!(asserted_file_contents, file_contents);
|
||||
} else {
|
||||
panic!("File not found");
|
||||
}
|
||||
|
||||
let delete_res = delete_file(&directory.file_path, file_name);
|
||||
assert!(delete_res.is_ok());
|
||||
} else {
|
||||
panic!("could not get interactor")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_multiple() {
|
||||
if let Ok(path) = CREATE_TMP_DIR() {
|
||||
let directory = Directory::new(path);
|
||||
|
||||
for i in 0..10 {
|
||||
let file_name = format!("file{}.txt", i);
|
||||
let write_res = write_file(&directory.file_path, file_name, "test");
|
||||
assert!(write_res.is_ok());
|
||||
}
|
||||
|
||||
if let Ok(files) = directory.get_files() {
|
||||
for i in 0..10 {
|
||||
let file_name = format!("file{}.txt", i);
|
||||
assert!(files.contains_key(&file_name));
|
||||
assert_eq!(files.get(&file_name).unwrap(), "test");
|
||||
}
|
||||
} else {
|
||||
panic!("Files not found");
|
||||
}
|
||||
|
||||
if let Ok(file_names) = directory.get_file_names() {
|
||||
for i in 0..10 {
|
||||
let file_name = format!("file{}.txt", i);
|
||||
assert!(file_names.contains(&file_name));
|
||||
}
|
||||
} else {
|
||||
panic!("File names not found");
|
||||
}
|
||||
|
||||
for i in 0..10 {
|
||||
let file_name = format!("file{}.txt", i);
|
||||
assert!(delete_file(&directory.file_path, file_name).is_ok());
|
||||
}
|
||||
} else {
|
||||
panic!("could not get interactor")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
use enostr::{Keypair, Pubkey};
|
||||
|
||||
use super::file_key_storage::FileKeyStorage;
|
||||
use crate::Error;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::security_framework_key_storage::SecurityFrameworkKeyStorage;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum KeyStorageType {
|
||||
None,
|
||||
FileSystem(FileKeyStorage),
|
||||
#[cfg(target_os = "macos")]
|
||||
SecurityFramework(SecurityFrameworkKeyStorage),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum KeyStorageResponse<R> {
|
||||
Waiting,
|
||||
ReceivedResult(Result<R, KeyStorageError>),
|
||||
}
|
||||
|
||||
impl<R: PartialEq> PartialEq for KeyStorageResponse<R> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(KeyStorageResponse::Waiting, KeyStorageResponse::Waiting) => true,
|
||||
(
|
||||
KeyStorageResponse::ReceivedResult(Ok(r1)),
|
||||
KeyStorageResponse::ReceivedResult(Ok(r2)),
|
||||
) => r1 == r2,
|
||||
(
|
||||
KeyStorageResponse::ReceivedResult(Err(_)),
|
||||
KeyStorageResponse::ReceivedResult(Err(_)),
|
||||
) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStorageType {
|
||||
pub fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(Vec::new())),
|
||||
Self::FileSystem(f) => f.get_keys(),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(f) => f.get_keys(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||
let _ = key;
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
||||
Self::FileSystem(f) => f.add_key(key),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(f) => f.add_key(key),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||
let _ = key;
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
||||
Self::FileSystem(f) => f.remove_key(key),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(f) => f.remove_key(key),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_key(&self) -> KeyStorageResponse<Option<Pubkey>> {
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(None)),
|
||||
Self::FileSystem(f) => f.get_selected_key(),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_key(&self, key: Option<Pubkey>) -> KeyStorageResponse<()> {
|
||||
match self {
|
||||
Self::None => KeyStorageResponse::ReceivedResult(Ok(())),
|
||||
Self::FileSystem(f) => f.select_key(key),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::SecurityFramework(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub enum KeyStorageError {
|
||||
Retrieval(Error),
|
||||
Addition(Error),
|
||||
Selection(Error),
|
||||
Removal(Error),
|
||||
OSError(Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeyStorageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Retrieval(e) => write!(f, "Failed to retrieve keys: {:?}", e),
|
||||
Self::Addition(key) => write!(f, "Failed to add key: {:?}", key),
|
||||
Self::Selection(pubkey) => write!(f, "Failed to select key: {:?}", pubkey),
|
||||
Self::Removal(key) => write!(f, "Failed to remove key: {:?}", key),
|
||||
Self::OSError(e) => write!(f, "OS had an error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for KeyStorageError {}
|
||||
@@ -9,10 +9,10 @@ use crate::{
|
||||
route::Route,
|
||||
timeline::{kind::ListKind, PubkeySource, Timeline, TimelineId, TimelineKind, TimelineRoute},
|
||||
ui::add_column::AddColumnRoute,
|
||||
Error,
|
||||
Result,
|
||||
};
|
||||
|
||||
use super::{DataPath, DataPathType, Directory};
|
||||
use notedeck::{DataPath, DataPathType, Directory};
|
||||
|
||||
pub static COLUMNS_FILE: &str = "columns.json";
|
||||
|
||||
@@ -123,7 +123,7 @@ struct MigrationColumn {
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MigrationColumn {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
@@ -266,9 +266,11 @@ pub fn deserialize_columns(path: &DataPath, ndb: &Ndb, user: Option<&[u8; 32]>)
|
||||
string_to_columns(columns_json(path)?, ndb, user)
|
||||
}
|
||||
|
||||
fn deserialize_columns_string(serialized_columns: String) -> Result<MigrationColumns, Error> {
|
||||
serde_json::from_str::<MigrationColumns>(&serialized_columns)
|
||||
.map_err(|e| Error::Generic(e.to_string()))
|
||||
fn deserialize_columns_string(serialized_columns: String) -> Result<MigrationColumns> {
|
||||
Ok(
|
||||
serde_json::from_str::<MigrationColumns>(&serialized_columns)
|
||||
.map_err(notedeck::Error::Json)?,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
mod decks;
|
||||
mod file_key_storage;
|
||||
mod file_storage;
|
||||
mod migration;
|
||||
|
||||
pub use decks::{load_decks_cache, save_decks_cache, DECKS_CACHE_FILE};
|
||||
pub use file_key_storage::FileKeyStorage;
|
||||
pub use file_storage::{delete_file, write_file, DataPath, DataPathType, Directory};
|
||||
pub use migration::{deserialize_columns, COLUMNS_FILE};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod security_framework_key_storage;
|
||||
|
||||
pub mod key_storage_impl;
|
||||
pub use key_storage_impl::{KeyStorageResponse, KeyStorageType};
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use enostr::{Keypair, Pubkey, SecretKey};
|
||||
use security_framework::{
|
||||
item::{ItemClass, ItemSearchOptions, Limit, SearchResult},
|
||||
passwords::{delete_generic_password, set_generic_password},
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
use super::{key_storage_impl::KeyStorageError, KeyStorageResponse};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct SecurityFrameworkKeyStorage {
|
||||
pub service_name: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl SecurityFrameworkKeyStorage {
|
||||
pub fn new(service_name: String) -> Self {
|
||||
SecurityFrameworkKeyStorage {
|
||||
service_name: Cow::Owned(service_name),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> {
|
||||
match set_generic_password(
|
||||
&self.service_name,
|
||||
key.pubkey.hex().as_str(),
|
||||
key.secret_key
|
||||
.as_ref()
|
||||
.map_or_else(|| &[] as &[u8], |sc| sc.as_secret_bytes()),
|
||||
) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(KeyStorageError::Addition(Error::Generic(e.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pubkey_strings(&self) -> Vec<String> {
|
||||
let search_results = ItemSearchOptions::new()
|
||||
.class(ItemClass::generic_password())
|
||||
.service(&self.service_name)
|
||||
.load_attributes(true)
|
||||
.limit(Limit::All)
|
||||
.search();
|
||||
|
||||
let mut accounts = Vec::new();
|
||||
|
||||
if let Ok(search_results) = search_results {
|
||||
for result in search_results {
|
||||
if let Some(map) = result.simplify_dict() {
|
||||
if let Some(val) = map.get("acct") {
|
||||
accounts.push(val.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accounts
|
||||
}
|
||||
|
||||
fn get_pubkeys(&self) -> Vec<Pubkey> {
|
||||
self.get_pubkey_strings()
|
||||
.iter_mut()
|
||||
.filter_map(|pubkey_str| Pubkey::from_hex(pubkey_str.as_str()).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_privkey_bytes_for(&self, account: &str) -> Option<Vec<u8>> {
|
||||
let search_result = ItemSearchOptions::new()
|
||||
.class(ItemClass::generic_password())
|
||||
.service(&self.service_name)
|
||||
.load_data(true)
|
||||
.account(account)
|
||||
.search();
|
||||
|
||||
if let Ok(results) = search_result {
|
||||
if let Some(SearchResult::Data(vec)) = results.first() {
|
||||
return Some(vec.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_secret_key_for_pubkey(&self, pubkey: &Pubkey) -> Option<SecretKey> {
|
||||
if let Some(bytes) = self.get_privkey_bytes_for(pubkey.hex().as_str()) {
|
||||
SecretKey::from_slice(bytes.as_slice()).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_all_keypairs(&self) -> Vec<Keypair> {
|
||||
self.get_pubkeys()
|
||||
.iter()
|
||||
.map(|pubkey| {
|
||||
let maybe_secret = self.get_secret_key_for_pubkey(pubkey);
|
||||
Keypair::new(*pubkey, maybe_secret)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn delete_key(&self, pubkey: &Pubkey) -> Result<(), KeyStorageError> {
|
||||
match delete_generic_password(&self.service_name, pubkey.hex().as_str()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("delete key error {}", e);
|
||||
Err(KeyStorageError::Removal(Error::Generic(e.to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecurityFrameworkKeyStorage {
|
||||
pub fn add_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||
KeyStorageResponse::ReceivedResult(self.add_key_internal(key))
|
||||
}
|
||||
|
||||
pub fn get_keys(&self) -> KeyStorageResponse<Vec<Keypair>> {
|
||||
KeyStorageResponse::ReceivedResult(Ok(self.get_all_keypairs()))
|
||||
}
|
||||
|
||||
pub fn remove_key(&self, key: &Keypair) -> KeyStorageResponse<()> {
|
||||
KeyStorageResponse::ReceivedResult(self.delete_key(&key.pubkey))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use enostr::FullKeypair;
|
||||
|
||||
static TEST_SERVICE_NAME: &str = "NOTEDECKTEST";
|
||||
static STORAGE: SecurityFrameworkKeyStorage = SecurityFrameworkKeyStorage {
|
||||
service_name: Cow::Borrowed(TEST_SERVICE_NAME),
|
||||
};
|
||||
|
||||
// individual tests are ignored so test runner doesn't run them all concurrently
|
||||
// TODO: a way to run them all serially should be devised
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn add_and_remove_test_pubkey_only() {
|
||||
let num_keys_before_test = STORAGE.get_pubkeys().len();
|
||||
|
||||
let keypair = FullKeypair::generate().to_keypair();
|
||||
let add_result = STORAGE.add_key_internal(&keypair);
|
||||
assert!(add_result.is_ok());
|
||||
|
||||
let get_pubkeys_result = STORAGE.get_pubkeys();
|
||||
assert_eq!(get_pubkeys_result.len() - num_keys_before_test, 1);
|
||||
|
||||
let remove_result = STORAGE.delete_key(&keypair.pubkey);
|
||||
assert!(remove_result.is_ok());
|
||||
|
||||
let keys = STORAGE.get_pubkeys();
|
||||
assert_eq!(keys.len() - num_keys_before_test, 0);
|
||||
}
|
||||
|
||||
fn add_and_remove_full_n(n: usize) {
|
||||
let num_keys_before_test = STORAGE.get_all_keypairs().len();
|
||||
// there must be zero keys in storage for the test to work as intended
|
||||
assert_eq!(num_keys_before_test, 0);
|
||||
|
||||
let expected_keypairs: Vec<Keypair> = (0..n)
|
||||
.map(|_| FullKeypair::generate().to_keypair())
|
||||
.collect();
|
||||
|
||||
expected_keypairs.iter().for_each(|keypair| {
|
||||
let add_result = STORAGE.add_key_internal(keypair);
|
||||
assert!(add_result.is_ok());
|
||||
});
|
||||
|
||||
let asserted_keypairs = STORAGE.get_all_keypairs();
|
||||
assert_eq!(expected_keypairs, asserted_keypairs);
|
||||
|
||||
expected_keypairs.iter().for_each(|keypair| {
|
||||
let remove_result = STORAGE.delete_key(&keypair.pubkey);
|
||||
assert!(remove_result.is_ok());
|
||||
});
|
||||
|
||||
let num_keys_after_test = STORAGE.get_all_keypairs().len();
|
||||
assert_eq!(num_keys_after_test, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn add_and_remove_full() {
|
||||
add_and_remove_full_n(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn add_and_remove_full_10() {
|
||||
add_and_remove_full_n(10);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use tracing::error;
|
||||
|
||||
use crate::storage::{DataPath, DataPathType, Directory};
|
||||
use notedeck::{DataPath, DataPathType, Directory};
|
||||
|
||||
pub struct Support {
|
||||
directory: Directory,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
use std::path::Path;
|
||||
|
||||
use enostr::{FullKeypair, Pubkey, RelayPool};
|
||||
use nostrdb::{ProfileRecord, Transaction};
|
||||
|
||||
use crate::{user_account::UserAccount, Damus};
|
||||
use enostr::RelayPool;
|
||||
use nostrdb::ProfileRecord;
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
pub fn sample_pool() -> RelayPool {
|
||||
@@ -70,6 +66,7 @@ pub fn test_profile_record() -> ProfileRecord<'static> {
|
||||
ProfileRecord::new_owned(&TEST_PROFILE_DATA).unwrap()
|
||||
}
|
||||
|
||||
/*
|
||||
const TEN_ACCOUNT_HEXES: [&str; 10] = [
|
||||
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681",
|
||||
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
|
||||
@@ -95,7 +92,7 @@ pub fn get_test_accounts() -> Vec<UserAccount> {
|
||||
}
|
||||
|
||||
pub fn test_app() -> Damus {
|
||||
let db_dir = Path::new(".");
|
||||
let db_dir = Path::new("target/testdbs/test_app");
|
||||
let path = db_dir.to_str().unwrap();
|
||||
let mut app = Damus::mock(path);
|
||||
|
||||
@@ -109,3 +106,4 @@ pub fn test_app() -> Damus {
|
||||
|
||||
app
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
use egui::ThemePreference;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::storage::{write_file, DataPath, DataPathType, Directory};
|
||||
|
||||
pub struct ThemeHandler {
|
||||
directory: Directory,
|
||||
fallback_theme: ThemePreference,
|
||||
}
|
||||
|
||||
const THEME_FILE: &str = "theme.txt";
|
||||
|
||||
impl ThemeHandler {
|
||||
pub fn new(path: &DataPath) -> Self {
|
||||
let directory = Directory::new(path.path(DataPathType::Setting));
|
||||
let fallback_theme = ThemePreference::Dark;
|
||||
Self {
|
||||
directory,
|
||||
fallback_theme,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&self) -> ThemePreference {
|
||||
match self.directory.get_file(THEME_FILE.to_owned()) {
|
||||
Ok(contents) => match deserialize_theme(contents) {
|
||||
Some(theme) => theme,
|
||||
None => {
|
||||
error!(
|
||||
"Could not deserialize theme. Using fallback {:?} instead",
|
||||
self.fallback_theme
|
||||
);
|
||||
self.fallback_theme
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Could not read {} file: {:?}\nUsing fallback {:?} instead",
|
||||
THEME_FILE, e, self.fallback_theme
|
||||
);
|
||||
self.fallback_theme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self, theme: ThemePreference) {
|
||||
match write_file(
|
||||
&self.directory.file_path,
|
||||
THEME_FILE.to_owned(),
|
||||
&theme_to_serialized(&theme),
|
||||
) {
|
||||
Ok(_) => info!(
|
||||
"Successfully saved {:?} theme change to {}",
|
||||
theme, THEME_FILE
|
||||
),
|
||||
Err(_) => error!("Could not save {:?} theme change to {}", theme, THEME_FILE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn theme_to_serialized(theme: &ThemePreference) -> String {
|
||||
match theme {
|
||||
ThemePreference::Dark => "dark",
|
||||
ThemePreference::Light => "light",
|
||||
ThemePreference::System => "system",
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn deserialize_theme(serialized_theme: String) -> Option<ThemePreference> {
|
||||
match serialized_theme.as_str() {
|
||||
"dark" => Some(ThemePreference::Dark),
|
||||
"light" => Some(ThemePreference::Light),
|
||||
"system" => Some(ThemePreference::System),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
use crate::{
|
||||
multi_subscriber::MultiSubscriber,
|
||||
muted::MuteFun,
|
||||
note::NoteRef,
|
||||
notecache::NoteCache,
|
||||
notes_holder::NotesHolder,
|
||||
timeline::{TimelineTab, ViewFilter},
|
||||
};
|
||||
|
||||
use nostrdb::{Filter, FilterBuilder, Ndb, Transaction};
|
||||
use notedeck::{MuteFun, NoteCache, NoteRef};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Thread {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub fn time_ago_since(timestamp: u64) -> String {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
// Determine if the timestamp is in the future or the past
|
||||
let duration = if now >= timestamp {
|
||||
now.saturating_sub(timestamp)
|
||||
} else {
|
||||
timestamp.saturating_sub(now)
|
||||
};
|
||||
|
||||
let future = timestamp > now;
|
||||
let relstr = if future { "+" } else { "" };
|
||||
|
||||
let years = duration / 31_536_000; // seconds in a year
|
||||
if years >= 1 {
|
||||
return format!("{}{}yr", relstr, years);
|
||||
}
|
||||
|
||||
let months = duration / 2_592_000; // seconds in a month (30.44 days)
|
||||
if months >= 1 {
|
||||
return format!("{}{}mth", relstr, months);
|
||||
}
|
||||
|
||||
let weeks = duration / 604_800; // seconds in a week
|
||||
if weeks >= 1 {
|
||||
return format!("{}{}wk", relstr, weeks);
|
||||
}
|
||||
|
||||
let days = duration / 86_400; // seconds in a day
|
||||
if days >= 1 {
|
||||
return format!("{}{}d", relstr, days);
|
||||
}
|
||||
|
||||
let hours = duration / 3600; // seconds in an hour
|
||||
if hours >= 1 {
|
||||
return format!("{}{}h", relstr, hours);
|
||||
}
|
||||
|
||||
let minutes = duration / 60; // seconds in a minute
|
||||
if minutes >= 1 {
|
||||
return format!("{}{}m", relstr, minutes);
|
||||
}
|
||||
|
||||
let seconds = duration;
|
||||
if seconds >= 3 {
|
||||
return format!("{}{}s", relstr, seconds);
|
||||
}
|
||||
|
||||
"now".to_string()
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TimeCached<T> {
|
||||
last_update: Instant,
|
||||
expires_in: Duration,
|
||||
value: Option<T>,
|
||||
refresh: Rc<dyn Fn() -> T + 'static>,
|
||||
}
|
||||
|
||||
impl<T> TimeCached<T> {
|
||||
pub fn new(expires_in: Duration, refresh: impl Fn() -> T + 'static) -> Self {
|
||||
TimeCached {
|
||||
last_update: Instant::now(),
|
||||
expires_in,
|
||||
value: None,
|
||||
refresh: Rc::new(refresh),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn needs_update(&self) -> bool {
|
||||
self.value.is_none() || self.last_update.elapsed() > self.expires_in
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
self.last_update = Instant::now();
|
||||
self.value = Some((self.refresh)());
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Option<&T> {
|
||||
self.value.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &T {
|
||||
if self.needs_update() {
|
||||
self.update();
|
||||
}
|
||||
self.value.as_ref().unwrap() // This unwrap is safe because we just set the value if it was None.
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::error::{Error, FilterError};
|
||||
use crate::filter;
|
||||
use crate::filter::FilterState;
|
||||
use crate::error::Error;
|
||||
use crate::timeline::Timeline;
|
||||
use enostr::{Filter, Pubkey};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{filter::default_limit, FilterError, FilterState};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
use tracing::{error, warn};
|
||||
@@ -111,7 +110,7 @@ impl TimelineKind {
|
||||
TimelineKind::Universe,
|
||||
FilterState::ready(vec![Filter::new()
|
||||
.kinds([1])
|
||||
.limit(filter::default_limit())
|
||||
.limit(default_limit())
|
||||
.build()]),
|
||||
)),
|
||||
|
||||
@@ -129,7 +128,7 @@ impl TimelineKind {
|
||||
let filter = Filter::new()
|
||||
.authors([pk])
|
||||
.kinds([1])
|
||||
.limit(filter::default_limit())
|
||||
.limit(default_limit())
|
||||
.build();
|
||||
|
||||
Some(Timeline::new(
|
||||
@@ -147,7 +146,7 @@ impl TimelineKind {
|
||||
let notifications_filter = Filter::new()
|
||||
.pubkeys([pk])
|
||||
.kinds([1])
|
||||
.limit(crate::filter::default_limit())
|
||||
.limit(default_limit())
|
||||
.build();
|
||||
|
||||
Some(Timeline::new(
|
||||
@@ -179,10 +178,12 @@ impl TimelineKind {
|
||||
}
|
||||
|
||||
match Timeline::contact_list(&results[0].note, pk_src.clone()) {
|
||||
Err(Error::Filter(FilterError::EmptyContactList)) => Some(Timeline::new(
|
||||
TimelineKind::contact_list(pk_src),
|
||||
FilterState::needs_remote(vec![contact_filter]),
|
||||
)),
|
||||
Err(Error::App(notedeck::Error::Filter(FilterError::EmptyContactList))) => {
|
||||
Some(Timeline::new(
|
||||
TimelineKind::contact_list(pk_src),
|
||||
FilterState::needs_remote(vec![contact_filter]),
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Unexpected error: {e}");
|
||||
None
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use crate::{
|
||||
column::Columns,
|
||||
decks::DecksCache,
|
||||
error::{Error, FilterError},
|
||||
filter::{self, FilterState, FilterStates},
|
||||
muted::MuteFun,
|
||||
note::NoteRef,
|
||||
notecache::{CachedNote, NoteCache},
|
||||
error::Error,
|
||||
subscriptions::{self, SubKind, Subscriptions},
|
||||
unknowns::UnknownIds,
|
||||
Result,
|
||||
};
|
||||
|
||||
use notedeck::{
|
||||
filter, CachedNote, FilterError, FilterState, FilterStates, MuteFun, NoteCache, NoteRef,
|
||||
UnknownIds,
|
||||
};
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
@@ -271,7 +271,9 @@ impl Timeline {
|
||||
let timeline = timelines
|
||||
.get_mut(timeline_idx)
|
||||
.ok_or(Error::TimelineNotFound)?;
|
||||
let sub = timeline.subscription.ok_or(Error::no_active_sub())?;
|
||||
let sub = timeline
|
||||
.subscription
|
||||
.ok_or(Error::App(notedeck::Error::no_active_sub()))?;
|
||||
|
||||
let new_note_ids = ndb.poll_for_notes(sub, 500);
|
||||
if new_note_ids.is_empty() {
|
||||
@@ -535,7 +537,7 @@ fn setup_initial_timeline(
|
||||
"querying nostrdb sub {:?} {:?}",
|
||||
timeline.subscription, timeline.filter
|
||||
);
|
||||
let lim = filters[0].limit().unwrap_or(crate::filter::default_limit()) as i32;
|
||||
let lim = filters[0].limit().unwrap_or(filter::default_limit()) as i32;
|
||||
let notes = ndb
|
||||
.query(&txn, filters, lim)?
|
||||
.into_iter()
|
||||
@@ -607,7 +609,7 @@ fn setup_timeline_nostrdb_sub(
|
||||
let filter_state = timeline
|
||||
.filter
|
||||
.get_any_ready()
|
||||
.ok_or(Error::empty_contact_list())?
|
||||
.ok_or(Error::App(notedeck::Error::empty_contact_list()))?
|
||||
.to_owned();
|
||||
|
||||
setup_initial_timeline(ndb, timeline, note_cache, &filter_state, is_muted)?;
|
||||
@@ -661,7 +663,7 @@ pub fn is_timeline_ready(
|
||||
|
||||
// TODO: into_follow_filter is hardcoded to contact lists, let's generalize
|
||||
match filter {
|
||||
Err(Error::Filter(e)) => {
|
||||
Err(notedeck::Error::Filter(e)) => {
|
||||
error!("got broken when building filter {e}");
|
||||
timeline
|
||||
.filter
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use crate::{
|
||||
accounts::Accounts,
|
||||
column::Columns,
|
||||
draft::Drafts,
|
||||
imgcache::ImageCache,
|
||||
muted::MuteFun,
|
||||
nav::RenderNavAction,
|
||||
notecache::NoteCache,
|
||||
notes_holder::NotesHolderStorage,
|
||||
profile::Profile,
|
||||
thread::Thread,
|
||||
@@ -15,11 +11,11 @@ use crate::{
|
||||
note::{NoteOptions, QuoteRepostView},
|
||||
profile::ProfileView,
|
||||
},
|
||||
unknowns::UnknownIds,
|
||||
};
|
||||
|
||||
use enostr::{NoteId, Pubkey};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, UnknownIds};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
pub enum TimelineRoute {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::app_style::NotedeckTextStyle;
|
||||
use crate::key_parsing::AcquireKeyError;
|
||||
use crate::login_manager::AcquireKeyState;
|
||||
use crate::ui::{Preview, PreviewConfig, View};
|
||||
use egui::TextEdit;
|
||||
use egui::{Align, Button, Color32, Frame, InnerResponse, Margin, RichText, Vec2};
|
||||
use enostr::Keypair;
|
||||
use notedeck::NotedeckTextStyle;
|
||||
|
||||
pub struct AccountLoginView<'a> {
|
||||
manager: &'a mut AcquireKeyState,
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
use crate::colors::PINK;
|
||||
use crate::imgcache::ImageCache;
|
||||
use crate::{
|
||||
accounts::Accounts,
|
||||
ui::{Preview, PreviewConfig, View},
|
||||
Damus,
|
||||
};
|
||||
use egui::{
|
||||
Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, UiBuilder, Vec2,
|
||||
};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{Accounts, ImageCache};
|
||||
|
||||
use super::profile::preview::SimpleProfilePreview;
|
||||
|
||||
@@ -180,7 +175,7 @@ fn scroll_area() -> ScrollArea {
|
||||
}
|
||||
|
||||
fn add_account_button() -> Button<'static> {
|
||||
let img_data = egui::include_image!("../../assets/icons/add_account_icon_4x.png");
|
||||
let img_data = egui::include_image!("../../../../assets/icons/add_account_icon_4x.png");
|
||||
let img = Image::new(img_data).fit_to_exact_size(Vec2::new(48.0, 48.0));
|
||||
Button::image_and_text(
|
||||
img,
|
||||
@@ -195,48 +190,3 @@ fn add_account_button() -> Button<'static> {
|
||||
fn sign_out_button() -> egui::Button<'static> {
|
||||
egui::Button::new(RichText::new("Sign out"))
|
||||
}
|
||||
|
||||
// PREVIEWS
|
||||
mod preview {
|
||||
|
||||
use super::*;
|
||||
use crate::{accounts::process_accounts_view_response, test_data};
|
||||
|
||||
pub struct AccountsPreview {
|
||||
app: Damus,
|
||||
}
|
||||
|
||||
impl AccountsPreview {
|
||||
fn new() -> Self {
|
||||
let app = test_data::test_app();
|
||||
AccountsPreview { app }
|
||||
}
|
||||
}
|
||||
|
||||
impl View for AccountsPreview {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_space(24.0);
|
||||
// TODO(jb55): maybe just use render_nav here so we can step through routes
|
||||
if let Some(response) =
|
||||
AccountsView::new(&self.app.ndb, &self.app.accounts, &mut self.app.img_cache)
|
||||
.ui(ui)
|
||||
.inner
|
||||
{
|
||||
process_accounts_view_response(
|
||||
&mut self.app.accounts,
|
||||
&mut self.app.decks_cache,
|
||||
0,
|
||||
response,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Preview for AccountsView<'_> {
|
||||
type Prev = AccountsPreview;
|
||||
|
||||
fn preview(_cfg: PreviewConfig) -> Self::Prev {
|
||||
AccountsPreview::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ use nostrdb::Ndb;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
app_style::{get_font_size, NotedeckTextStyle},
|
||||
login_manager::AcquireKeyState,
|
||||
timeline::{PubkeySource, Timeline, TimelineKind},
|
||||
ui::anim::ICON_EXPANSION_MULTIPLE,
|
||||
user_account::UserAccount,
|
||||
Damus,
|
||||
};
|
||||
|
||||
use notedeck::{AppContext, NotedeckTextStyle, UserAccount};
|
||||
|
||||
use super::{anim::AnimationHelper, padding};
|
||||
|
||||
pub enum AddColumnResponse {
|
||||
@@ -180,8 +180,8 @@ impl<'a> AddColumnView<'a> {
|
||||
let max_width = ui.available_width();
|
||||
let title_style = NotedeckTextStyle::Body;
|
||||
let desc_style = NotedeckTextStyle::Button;
|
||||
let title_min_font_size = get_font_size(ui.ctx(), &title_style);
|
||||
let desc_min_font_size = get_font_size(ui.ctx(), &desc_style);
|
||||
let title_min_font_size = notedeck::fonts::get_font_size(ui.ctx(), &title_style);
|
||||
let desc_min_font_size = notedeck::fonts::get_font_size(ui.ctx(), &desc_style);
|
||||
|
||||
let max_height = {
|
||||
let max_wrap_width =
|
||||
@@ -279,7 +279,7 @@ impl<'a> AddColumnView<'a> {
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Universe",
|
||||
description: "See the whole nostr universe",
|
||||
icon: egui::include_image!("../../assets/icons/universe_icon_dark_4x.png"),
|
||||
icon: egui::include_image!("../../../../assets/icons/universe_icon_dark_4x.png"),
|
||||
option: AddColumnOption::Universe,
|
||||
});
|
||||
|
||||
@@ -293,20 +293,20 @@ impl<'a> AddColumnView<'a> {
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Home timeline",
|
||||
description: "See recommended notes first",
|
||||
icon: egui::include_image!("../../assets/icons/home_icon_dark_4x.png"),
|
||||
icon: egui::include_image!("../../../../assets/icons/home_icon_dark_4x.png"),
|
||||
option: AddColumnOption::Home(source.clone()),
|
||||
});
|
||||
}
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Notifications",
|
||||
description: "Stay up to date with notifications and mentions",
|
||||
icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"),
|
||||
icon: egui::include_image!("../../../../assets/icons/notifications_icon_dark_4x.png"),
|
||||
option: AddColumnOption::UndecidedNotification,
|
||||
});
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Hashtag",
|
||||
description: "Stay up to date with a certain hashtag",
|
||||
icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"),
|
||||
icon: egui::include_image!("../../../../assets/icons/notifications_icon_dark_4x.png"),
|
||||
option: AddColumnOption::UndecidedHashtag,
|
||||
});
|
||||
|
||||
@@ -326,7 +326,9 @@ impl<'a> AddColumnView<'a> {
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Your Notifications",
|
||||
description: "Stay up to date with your notifications and mentions",
|
||||
icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"),
|
||||
icon: egui::include_image!(
|
||||
"../../../../assets/icons/notifications_icon_dark_4x.png"
|
||||
),
|
||||
option: AddColumnOption::Notification(source),
|
||||
});
|
||||
}
|
||||
@@ -334,7 +336,7 @@ impl<'a> AddColumnView<'a> {
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Someone else's Notifications",
|
||||
description: "Stay up to date with someone else's notifications and mentions",
|
||||
icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"),
|
||||
icon: egui::include_image!("../../../../assets/icons/notifications_icon_dark_4x.png"),
|
||||
option: AddColumnOption::ExternalNotification,
|
||||
});
|
||||
|
||||
@@ -352,19 +354,20 @@ struct ColumnOptionData {
|
||||
pub fn render_add_column_routes(
|
||||
ui: &mut egui::Ui,
|
||||
app: &mut Damus,
|
||||
ctx: &mut AppContext<'_>,
|
||||
col: usize,
|
||||
route: &AddColumnRoute,
|
||||
) {
|
||||
let mut add_column_view = AddColumnView::new(
|
||||
&mut app.view_state.id_state_map,
|
||||
&app.ndb,
|
||||
app.accounts.get_selected_account(),
|
||||
ctx.ndb,
|
||||
ctx.accounts.get_selected_account(),
|
||||
);
|
||||
let resp = match route {
|
||||
AddColumnRoute::Base => add_column_view.ui(ui),
|
||||
AddColumnRoute::UndecidedNotification => add_column_view.notifications_ui(ui),
|
||||
AddColumnRoute::ExternalNotification => add_column_view.external_notification_ui(ui),
|
||||
AddColumnRoute::Hashtag => hashtag_ui(ui, &app.ndb, &mut app.view_state.id_string_map),
|
||||
AddColumnRoute::Hashtag => hashtag_ui(ui, ctx.ndb, &mut app.view_state.id_string_map),
|
||||
};
|
||||
|
||||
if let Some(resp) = resp {
|
||||
@@ -372,27 +375,34 @@ pub fn render_add_column_routes(
|
||||
AddColumnResponse::Timeline(mut timeline) => {
|
||||
crate::timeline::setup_new_timeline(
|
||||
&mut timeline,
|
||||
&app.ndb,
|
||||
ctx.ndb,
|
||||
&mut app.subscriptions,
|
||||
&mut app.pool,
|
||||
&mut app.note_cache,
|
||||
ctx.pool,
|
||||
ctx.note_cache,
|
||||
app.since_optimize,
|
||||
&app.accounts.mutefun(),
|
||||
&ctx.accounts.mutefun(),
|
||||
);
|
||||
app.columns_mut().add_timeline_to_column(col, timeline);
|
||||
app.columns_mut(ctx.accounts)
|
||||
.add_timeline_to_column(col, timeline);
|
||||
}
|
||||
AddColumnResponse::UndecidedNotification => {
|
||||
app.columns_mut().column_mut(col).router_mut().route_to(
|
||||
crate::route::Route::AddColumn(AddColumnRoute::UndecidedNotification),
|
||||
);
|
||||
app.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.route_to(crate::route::Route::AddColumn(
|
||||
AddColumnRoute::UndecidedNotification,
|
||||
));
|
||||
}
|
||||
AddColumnResponse::ExternalNotification => {
|
||||
app.columns_mut().column_mut(col).router_mut().route_to(
|
||||
crate::route::Route::AddColumn(AddColumnRoute::ExternalNotification),
|
||||
);
|
||||
app.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.route_to(crate::route::Route::AddColumn(
|
||||
AddColumnRoute::ExternalNotification,
|
||||
));
|
||||
}
|
||||
AddColumnResponse::Hashtag => {
|
||||
app.columns_mut()
|
||||
app.columns_mut(ctx.accounts)
|
||||
.column_mut(col)
|
||||
.router_mut()
|
||||
.route_to(crate::route::Route::AddColumn(AddColumnRoute::Hashtag));
|
||||
@@ -438,44 +448,3 @@ pub fn hashtag_ui(
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
||||
mod preview {
|
||||
use crate::{
|
||||
test_data,
|
||||
ui::{Preview, PreviewConfig, View},
|
||||
Damus,
|
||||
};
|
||||
|
||||
use super::AddColumnView;
|
||||
|
||||
pub struct AddColumnPreview {
|
||||
app: Damus,
|
||||
}
|
||||
|
||||
impl AddColumnPreview {
|
||||
fn new() -> Self {
|
||||
let app = test_data::test_app();
|
||||
|
||||
AddColumnPreview { app }
|
||||
}
|
||||
}
|
||||
|
||||
impl View for AddColumnPreview {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
AddColumnView::new(
|
||||
&mut self.app.view_state.id_state_map,
|
||||
&self.app.ndb,
|
||||
self.app.accounts.get_selected_account(),
|
||||
)
|
||||
.ui(ui);
|
||||
}
|
||||
}
|
||||
|
||||
impl Preview for AddColumnView<'_> {
|
||||
type Prev = AddColumnPreview;
|
||||
|
||||
fn preview(_cfg: PreviewConfig) -> Self::Prev {
|
||||
AddColumnPreview::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::{
|
||||
app_style::NotedeckTextStyle,
|
||||
column::Columns,
|
||||
imgcache::ImageCache,
|
||||
nav::RenderNavAction,
|
||||
route::Route,
|
||||
timeline::{TimelineId, TimelineRoute},
|
||||
@@ -14,6 +12,7 @@ use crate::{
|
||||
use egui::{RichText, Stroke, UiBuilder};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{ImageCache, NotedeckTextStyle};
|
||||
|
||||
pub struct NavTitle<'a> {
|
||||
ndb: &'a Ndb,
|
||||
@@ -124,9 +123,9 @@ impl<'a> NavTitle<'a> {
|
||||
let max_size = icon_width * ICON_EXPANSION_MULTIPLE;
|
||||
|
||||
let img_data = if ui.visuals().dark_mode {
|
||||
egui::include_image!("../../../assets/icons/column_delete_icon_4x.png")
|
||||
egui::include_image!("../../../../../assets/icons/column_delete_icon_4x.png")
|
||||
} else {
|
||||
egui::include_image!("../../../assets/icons/column_delete_icon_light_4x.png")
|
||||
egui::include_image!("../../../../../assets/icons/column_delete_icon_light_4x.png")
|
||||
};
|
||||
let img = egui::Image::new(img_data).max_width(img_size);
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
use crate::{app_style::deck_icon_font_sized, colors::PINK, deck_state::DeckState};
|
||||
use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget};
|
||||
|
||||
use crate::{
|
||||
app_style::{deck_icon_font_sized, get_font_size, NotedeckTextStyle},
|
||||
colors::PINK,
|
||||
deck_state::DeckState,
|
||||
fonts::NamedFontFamily,
|
||||
};
|
||||
use notedeck::{NamedFontFamily, NotedeckTextStyle};
|
||||
|
||||
use super::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
@@ -39,7 +34,7 @@ impl<'a> ConfigureDeckView<'a> {
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui) -> Option<ConfigureDeckResponse> {
|
||||
let title_font = egui::FontId::new(
|
||||
get_font_size(ui.ctx(), &NotedeckTextStyle::Heading4),
|
||||
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Heading4),
|
||||
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||
);
|
||||
padding(16.0, ui, |ui| {
|
||||
@@ -52,7 +47,10 @@ impl<'a> ConfigureDeckView<'a> {
|
||||
ui.add(Label::new(
|
||||
RichText::new("We recommend short names")
|
||||
.color(ui.visuals().noninteractive().fg_stroke.color)
|
||||
.size(get_font_size(ui.ctx(), &NotedeckTextStyle::Small)),
|
||||
.size(notedeck::fonts::get_font_size(
|
||||
ui.ctx(),
|
||||
&NotedeckTextStyle::Small,
|
||||
)),
|
||||
));
|
||||
|
||||
ui.add_space(32.0);
|
||||
|
||||