Compare commits

...

75 commits

Author SHA1 Message Date
Ryan Stafford
a96b5b1cd2 fix dark default
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-08-08 18:03:03 -04:00
Ryan Stafford
d8292bb9be fix thumbnail default
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-08-08 17:57:47 -04:00
Ryan Stafford
3e1e3868f4 default settings, hide thumbnails option, github link. fixes #64, fixes #66, fixes #67
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-08-08 17:36:52 -04:00
Ryan Stafford
a91b08547b fix reply preview 500
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-31 14:41:55 -04:00
Ryan Stafford
624b7e4847 add fedilink. fixes #57
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-28 17:00:58 -04:00
Ryan Stafford
c935ffabea add version to make 2023-07-28 13:36:03 -04:00
Ryan Stafford
f5a423f2d5 remove mailto links from title
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-28 13:04:41 -04:00
Ryan Stafford
c5a53c79da show lemmy back end version
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-28 12:51:17 -04:00
Ryan Stafford
669749c12e fix settings save
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-26 22:33:06 -04:00
Ryan Stafford
dd58cb7e55 added new files
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-26 21:30:19 -04:00
Ryan Stafford
2bea76c7f0 fix expando classname spacing. fixes #42 2023-07-26 21:27:53 -04:00
Ryan Stafford
a689604470 hide removed posts. fixes #53 2023-07-26 21:25:55 -04:00
Ryan Stafford
6c80f67535 comment preview, image upload, versioned assets 2023-07-26 21:07:39 -04:00
Ryan Stafford
463b3fe49d don't try to rewrite links with anchors. fixes #45 2023-07-26 21:06:39 -04:00
Ryan Stafford
3fe31bd5b3 livereload 2023-07-26 16:58:22 -04:00
Ryan Stafford
1858f1aec0 fix community bangs. fixes #44
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-24 00:24:29 -04:00
Ryan Stafford
dd4f76a393 shrink version size
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-23 23:26:25 -04:00
Ryan Stafford
f7c2910b07 fix community bangs 2023-07-23 23:19:49 -04:00
Ryan Stafford
556cc785f5 ignore old subdomain when rewriting lemmy links 2023-07-23 23:07:57 -04:00
Ryan Stafford
d5a96181d2 remove excess community api calls 2023-07-23 22:16:42 -04:00
Ryan Stafford
3ebf9b3793 show mlmym version 2023-07-23 18:19:53 -04:00
Ryan Stafford
3ce346d815 communities redirect fix
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-23 15:21:44 -04:00
Ryan Stafford
9525c1ff4d redirect frontpage login errors 2023-07-23 15:05:31 -04:00
Ryan Stafford
b6ce673292 fix search paging, empty subscribed community search, my subscriptions dropdown. fixes #39 2023-07-23 14:52:44 -04:00
Ryan Stafford
ed7d9422d7 no comment jerk 2023-07-20 20:28:07 -04:00
Ryan Stafford
5d778161ec mark post read. fixes #20 2023-07-20 20:08:26 -04:00
Ryan Stafford
1eec1a7274 fix user settings 2023-07-20 19:35:42 -04:00
Ryan Stafford
8c7aabab72 spoiler fix, default search options 2023-07-16 13:45:40 -04:00
Ryan Stafford
0f4242e61e localize post and comment links
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-16 09:00:49 -04:00
Ryan Stafford
9782144dcb rewrite fix 2023-07-15 23:05:56 -04:00
Ryan Stafford
23d08a730c try remote instance link 2023-07-15 22:07:19 -04:00
Ryan Stafford
2d8a3d2315 localize community and user links, regex cleanup 2023-07-15 21:25:57 -04:00
Ryan Stafford
1dd8476fae community block button, link rewrite improvements, more imgur thumbnails #17
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-15 11:12:10 -04:00
Ryan Stafford
8d31cbada5 hide instance names, default comment sort options. fixes #32 2023-07-14 11:10:27 -04:00
Ryan Stafford
37e62d2e5f remove redundant child counts, only insert new
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-13 20:20:54 -04:00
Ryan Stafford
2b0eefa172 fix non local community search 2023-07-13 16:05:16 -04:00
Ryan Stafford
ddf25011cc context and parent comment links 2023-07-13 09:45:51 -04:00
Ryan Stafford
7b423eebb7 save post xhr. fixes #33
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-12 11:23:13 -04:00
Ryan Stafford
ca47acec84 hide all comments logic fix 2023-07-11 14:00:04 -04:00
Ryan Stafford
dcc3cd4d49 noscript pager fixes 2023-07-11 13:50:37 -04:00
Ryan Stafford
b9cf0c4835 seperated comment paging logic. fixes #29 fixes #30 2023-07-11 11:30:20 -04:00
Ryan Stafford
36df7969e6 fix hide children firing 2023-07-11 10:34:12 -04:00
Ryan Stafford
919a114f99 add missing handlers. fixes #31 2023-07-11 10:21:57 -04:00
Ryan Stafford
7ca1e23acf csp and http only cookies
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-10 12:52:56 -04:00
Ryan Stafford
bf0bc421cd add link to thumbnail fixes #19, load more error display
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-08 22:44:03 -04:00
Ryan Stafford
6685badaca insert youtube embeds on loadmore 2023-07-08 22:30:22 -04:00
Ryan Stafford
34f349313f reply fixes #25, endless scrolling settings fixes #18, settings popup. 2023-07-08 17:31:51 -04:00
Ryan Stafford
08a7ec66d4 gitignore 2023-07-08 11:28:37 -04:00
Ryan Stafford
f2ca245b13 xhr errorcallback, login and comment score/votes in inbox 2023-07-08 11:26:41 -04:00
Ryan Stafford
1b5c6f619e fix post links in inbox. fixes #21
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-08 10:44:09 -04:00
Ryan Stafford
d907420e58 view images toggle 2023-07-07 14:54:19 -04:00
Ryan Stafford
0a4888906a more thumbnails 2023-07-07 13:57:01 -04:00
Ryan Stafford
e46820ed3f display imgur thumbnails. fixes #17 2023-07-07 13:42:57 -04:00
Ryan Stafford
48d12530fe fix minimized comment padding, loadmore end jerk 2023-07-07 13:10:52 -04:00
Ryan Stafford
4fcfda174d endless scrolling, style fixes 2023-07-07 12:30:07 -04:00
Ryan Stafford
2a9a54f595 make settings global 2023-07-07 12:27:44 -04:00
Ryan Stafford
7760d7c226 fix login page 404 in single instance mode. fixes #16 2023-07-07 12:26:29 -04:00
Ryan Stafford
8e270007f4 support multiple spoiler tags 2023-07-07 12:25:40 -04:00
Ryan Stafford
4991a2e5ed reverted ambiguos text styles
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-06 19:02:48 -04:00
Ryan Stafford
f0754e3b20 parse markdown tables. fixes #14 2023-07-06 12:12:47 -04:00
Ryan Stafford
7c5ab065d0 lemmyfied style, spoiler tags, vote loading indicator. fixes #13 2023-07-06 12:08:05 -04:00
Ryan Stafford
3d23c96166 load more button 2023-07-05 18:43:54 -04:00
Ryan Stafford
ae0c8427d2 hide all toggle, get comments/posts bugfixes
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-05 16:59:23 -04:00
Ryan Stafford
834d8170d1 expando image resizing 2023-07-05 16:31:18 -04:00
Ryan Stafford
88457d0818 hide child comments button 2023-07-05 15:26:26 -04:00
Ryan Stafford
2b5349466a use full community name in top header links. fixes #10 2023-07-05 14:50:52 -04:00
Ryan Stafford
c105dfc900 fix saved tab not loading 2023-07-05 14:27:49 -04:00
Ryan Stafford
95195d00d7 add missing template
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-05 14:05:32 -04:00
Ryan Stafford
6e121dd63c remove markdown from post titles
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-05 11:22:46 -04:00
Ryan Stafford
3643600cab rewrite relative links, linkify community bangs. fixes #9 2023-07-05 11:01:10 -04:00
Ryan Stafford
f1f2305e82 fix youtube link false positive 2023-07-05 11:00:14 -04:00
Ryan Stafford
5a24937781 comment/post saving. fixes #6 2023-07-05 10:20:21 -04:00
Ryan Stafford
6184bddd02 distinguished posts/comemnts, nsfw blue/warnings 2023-07-05 09:49:41 -04:00
Ryan Stafford
1e0f1fc184 remove debug logging 2023-07-04 01:23:25 -04:00
Ryan Stafford
8668878548
link rewrite logic fix
Some checks are pending
Docker / docker-images (push) Waiting to run
2023-07-04 01:11:50 -04:00
30 changed files with 1936 additions and 464 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
mlmym
VERSION
*.toml
*.txt

View file

@ -4,6 +4,7 @@ WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . ./
RUN git describe --tag > VERSION
RUN go build -v -o mlmym
FROM debian:bullseye-slim
@ -14,4 +15,5 @@ RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -
COPY --from=builder /app/mlmym /app/mlmym
COPY --from=builder /app/templates /app/templates
COPY --from=builder /app/public /app/public
COPY --from=builder /app/VERSION /app/VERSION
CMD ["./mlmym", "--addr", "0.0.0.0:8080"]

View file

@ -1,17 +1,18 @@
.PHONY: dev reload serve style
.PHONY: dev reload serve VERSION
all:
$(MAKE) -j3 --no-print-directory dev
all: mlmym
dev: reload serve style
mlmym: VERSION
go build -v -o mlmym
dev:
$(MAKE) -j2 --no-print-directory reload serve
reload:
#websocketd --port=8080 watchexec -w public echo reload &>/dev/null
websocketd --loglevel=fatal --port=8009 watchexec --no-vcs-ignore -e html,css,js -d 500 -w public 'echo "$$WATCHEXEC_WRITTEN_PATH"'
websocketd --loglevel=fatal --port=8009 watchexec --no-vcs-ignore -e html,css,js 'echo "$$WATCHEXEC_WRITTEN_PATH"'
serve:
#python -m http.server --directory ./public 8081 &>/dev/null
watchexec -e go -r "go run . --addr 0.0.0.0:8008 -w"
VERSION:
git describe --tag > $@
style:
npm run watchcss > /dev/null 2>&1
serve: VERSION
DEBUG=true watchexec --no-vcs-ignore -e go -r "go run . --addr 0.0.0.0:8008 -w"

View file

@ -1,16 +1,25 @@
# mlmym
a familiar desktop experience for [lemmy](https://join-lemmy.org).
![screenshot](https://raw.githubusercontent.com/rystaf/mlmym/main/screenshot.png?raw=true)
![screenshot](https://raw.githubusercontent.com/rystaf/mlmym/main/screenshot1.png?raw=true)
### deployment
## deployment
```bash
docker run -it -p "8080:8080" ghcr.io/rystaf/mlmym:latest
```
### config
## config
Set the environment variable `LEMMY_DOMAIN` to run in single instance mode
```bash
docker run -it -e LEMMY_DOMAIN='lemmydomain.com' -p "8080:8080" ghcr.io/rystaf/mlmym:latest
```
#### default user settings
| environment variable | default |
| -------------------- | ------- |
| DARK | false |
| HIDE_THUMBNAILS | false |
| LISTING | All |
| SORT | Hot |
| COMMENT_SORT | Hot |

23
go.mod
View file

@ -3,15 +3,16 @@ module mlmym
go 1.19
require (
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/rystaf/go-lemmy v0.0.0-20230704005320-c4b010dd339b // indirect
github.com/yuin/goldmark v1.5.4 // indirect
go.elara.ws/go-lemmy v0.17.3 // indirect
golang.org/x/text v0.10.0 // indirect
github.com/dustin/go-humanize v1.0.1
github.com/julienschmidt/httprouter v1.3.0
github.com/k3a/html2text v1.2.1
github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968
github.com/yuin/goldmark v1.5.4
golang.org/x/text v0.10.0
)
require (
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
)

42
go.sum
View file

@ -2,39 +2,33 @@ github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+M
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/rystaf/go-lemmy v0.0.0-20230622213726-c394de37235c h1:VxOcsDMWaqoBKbhoiSBxPl1zZ62YZ/VAW2nxlBRJiow=
github.com/rystaf/go-lemmy v0.0.0-20230622213726-c394de37235c/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/rystaf/go-lemmy v0.0.0-20230622214853-5f2ab0756865 h1:xitFpcTOSP8RlZWR569yY75B2/7WX08rQQVG+0Mi4SA=
github.com/rystaf/go-lemmy v0.0.0-20230622214853-5f2ab0756865/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/rystaf/go-lemmy v0.0.0-20230622215253-d38b61ec174f h1:EueAC5v+8oX9xK9bT36Tpgbz+c66wUZx5zmyxePurbw=
github.com/rystaf/go-lemmy v0.0.0-20230622215253-d38b61ec174f/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/rystaf/go-lemmy v0.0.0-20230622222647-983d49e1d285 h1:tihBOF3ejTXzYVftaflwqRAXnaY4W9q3iNiE3YMF+D8=
github.com/rystaf/go-lemmy v0.0.0-20230622222647-983d49e1d285/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/rystaf/go-lemmy v0.0.0-20230622230518-ee2cfdf288a4 h1:++T5SoZzghtfNJprWlXiRSpPPdnMSSZgIWWAnPoGx/w=
github.com/rystaf/go-lemmy v0.0.0-20230622230518-ee2cfdf288a4/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/rystaf/go-lemmy v0.0.0-20230623185656-962f9bf8359d h1:ORS2KIBuT+wBn4wJncF1SoLDCVCAUPHASHpQ+Y3TnRI=
github.com/rystaf/go-lemmy v0.0.0-20230623185656-962f9bf8359d/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/rystaf/go-lemmy v0.0.0-20230623191111-7ff8c74b1935 h1:zmzUz6PGRB8yQTT6BRaZNTgNlrk6L7e72dzTnWJTw+I=
github.com/rystaf/go-lemmy v0.0.0-20230623191111-7ff8c74b1935/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/rystaf/go-lemmy v0.0.0-20230623191350-f39e3c8bdcb5 h1:MoI87uid2KqpLdUMZGK2HBOuxJMnPOJaar/4Og2PshM=
github.com/rystaf/go-lemmy v0.0.0-20230623191350-f39e3c8bdcb5/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/rystaf/go-lemmy v0.0.0-20230704005320-c4b010dd339b h1:6z+gOUUvKwKQfgqEbxXS229gjr5V3HYg9bYbL9VHFdQ=
github.com/rystaf/go-lemmy v0.0.0-20230704005320-c4b010dd339b/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY=
github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968 h1:wfyB6wQzYMH2U8xQvdamExbyCyPhe4o8HP47FMZM5Jk=
github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.elara.ws/go-lemmy v0.17.3 h1:644k23BS2xqKJHJ9cHd8eyt1INpb5myqsBQQL2chBiA=
go.elara.ws/go-lemmy v0.17.3/go.mod h1:rurQND/HT3yWfX/T4w+hb6vEwRAeAlV+9bSGFkkx5rA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

50
main.go
View file

@ -7,12 +7,14 @@ import (
"log"
"net"
"net/http"
"os"
"github.com/julienschmidt/httprouter"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
)
var version string
var watch = flag.Bool("w", false, "watch for file changes")
var addr = flag.String("addr", ":80", "http service address")
var md goldmark.Markdown
@ -48,10 +50,13 @@ func NewAddHeaderTransport(remoteAddr string) *AddHeaderTransport {
}
func init() {
md = goldmark.New(goldmark.WithExtensions(extension.Linkify))
md = goldmark.New(goldmark.WithExtensions(
extension.Linkify,
extension.Table,
))
templates = make(map[string]*template.Template)
if !*watch {
for _, name := range []string{"index.html", "login.html", "frontpage.html", "root.html", "settings.html"} {
for _, name := range []string{"index.html", "login.html", "frontpage.html", "root.html", "settings.html", "xhr.html", "create_comment.html"} {
t := template.New(name).Funcs(funcMap)
glob, err := t.ParseGlob("templates/*")
if err != nil {
@ -61,6 +66,47 @@ func init() {
templates[name] = glob
}
}
if os.Getenv("DEBUG") != "" {
test()
}
if data, err := os.ReadFile("VERSION"); err == nil {
version = string(data)
}
}
func test() {
links := [][]string{
[]string{"https://lemmy.local/u/dude", "/lemmy.local/u/dude", "/u/dude"},
[]string{"https://lemmy.local/u/dude@lemmy.local", "/lemmy.local/u/dude", "/u/dude"},
[]string{"/u/dude", "/lemmy.local/u/dude", "/u/dude"},
[]string{"/u/dude@lemmy.world", "/lemmy.local/u/dude@lemmy.world", "/u/dude@lemmy.world"},
[]string{"/u/dude@lemmy.local", "/lemmy.local/u/dude", "/u/dude"},
[]string{"https://lemmy.world/c/dude", "/lemmy.local/c/dude@lemmy.world", "/c/dude@lemmy.world"},
[]string{"https://lemmy.world/u/dude", "/lemmy.local/u/dude@lemmy.world", "/u/dude@lemmy.world"},
[]string{"https://lemmy.world/u/dude@lemmy.world", "/lemmy.local/u/dude@lemmy.world", "/u/dude@lemmy.world"},
[]string{"https://lemmy.world/post/123", "/lemmy.local/post/123@lemmy.world", "/post/123@lemmy.world"},
[]string{"https://lemmy.world/post/123#123", "https://lemmy.world/post/123#123", "https://lemmy.world/post/123#123"},
[]string{"/post/123", "/lemmy.local/post/123", "/post/123"},
[]string{"/comment/123", "/lemmy.local/comment/123", "/comment/123"},
[]string{"https://lemmy.local/comment/123", "/lemmy.local/comment/123", "/comment/123"},
}
for _, url := range links {
output := LemmyLinkRewrite(`href="`+url[0]+`"`, "lemmy.local", "")
success := (output == (`href="` + url[1] + `"`))
if !success {
fmt.Println("\n!!!! multi instance link rewrite failure !!!!")
fmt.Println(url)
fmt.Println(output)
fmt.Println("")
}
output = LemmyLinkRewrite(`href="`+url[0]+`"`, ".", "lemmy.local")
success = (output == (`href="` + url[2] + `"`))
if !success {
fmt.Println("\n!!!! single instance link rewrite failure !!!!")
fmt.Println(success, url)
fmt.Println(output)
fmt.Println("")
}
}
}
func middleware(n httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {

17
public/noscript.css Normal file
View file

@ -0,0 +1,17 @@
.scripting,
.expando-button,
.minimize,
#showimages,
#lmc,
.hidechildren {
display: none !important;
}
.post .expando .image img {
visibility: visible;
}
div.pager {
display: block;
}
.savecomment input[type=file] {
display: inline-block;
}

View file

@ -10,6 +10,9 @@ body.dark {
code {
word-wrap: break-word;
}
summary {
cursor: pointer;
}
.dark a {
color: #8cb3d9;
}
@ -24,24 +27,38 @@ code {
.clearleft {
clear: left;
}
.img-blur {
filter: blur(10px);
-webkit-filter: blur(10px);
-moz-filter: blur(10px);
-o-filter: blur(10px);
-ms-filter: blur(10px);
transform: scale(1.03);
}
.post {
margin: 6px;
margin: 6px 0px;
}
.post .thumb {
height: 52px;
width: 70px;
margin: 0px 4px;
position: relative;
overflow: hidden;
--background-color: #e7e7e7;
}
.post .thumb div {
height: 52px;
width: 70px;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
--background-color: #e7e7e7;
}
.rank {
color: #c6c6c6;
font-size: medium;
font-size: 14px;
margin-top: 17px;
text-align: right;
min-width: 19px;
min-width: 21px;
}
.dark .post .rank {
color: #646464;
@ -57,52 +74,67 @@ code {
color: #646464;
}
.comment .score {
margin-right: 2px;
clear:left;
margin-right: 4px;
overflow:hidden;
}
.comment .content {
line-height: 20px;
overflow:hidden;
max-width: 840px;
}
.comment.hidden {
padding-bottom:5px;
}
.comment.hidden .score {
display: none;
visibility: hidden;
}
.score form.link-btn input {
display: block;
display: inline-block;
color: #b7b7b7;
font-size: 20px;
line-height: 16px;
line-height: 15px;
}
.dark .score form.link-btn input {
color: #646464;
}
.comment form.link-btn input {
.comment .score form.link-btn input {
line-height: 17px;
}
.score form.like.link-btn input:first-child, .score .like div {
color: orangered;
color: #3880ff;
}
.score form.dislike.link-btn input:last-child, .score .dislike div {
color: #8080FF;
color: #eb445a;
}
.score form div {
position: relative;
top: 2px;
}
.title a {
color: #0000ff;
font-size: medium;
text-decoration: none;
}
.title a p {
display: inline;
}
.post.distinguished .title a, .post.announcement .title a,
.dark .post.distinguished .title a:visited, .post.announcement .title a:visited {
color: #228822;
font-weight: bold;
}
.dark .title a {
color: #dedede;
}
.dark .title a:visited {
color: #a6a6a6
}
.post.deleted .title a {
text-decoration: line-through;
}
[disabled] {
cursor: not-allowed;
}
#loadmore [disabled], .link-btn [disabled] {
cursor: wait;
}
.post .title {
color: #888;
font-size: 10px;
@ -131,8 +163,8 @@ code {
.message b {
color: #000;
}
.meta {
color: #888;
.dark .message b {
color: #ddd;
}
.dark .meta {
color: #b4b4b4;
@ -140,10 +172,10 @@ code {
.comment {
font-size: 14px;
margin: 0px 0px 5px 15px;
margin: 0px 0px 5px 0px;
border: 1px solid #e6e6e6;
border-radius: 3px;
padding: 5px 10px 5px 5px;
padding: 10px 10px 0px 7px;
}
.dark .comment {
border-color: #333;
@ -155,7 +187,6 @@ code {
max-height: 300px;
}
.comment .comment {
margin-left: 15px;
}
.comment .comment,
.comment .comment .comment .comment,
@ -186,9 +217,30 @@ code {
}
.comment .meta {
font-size: 10px;
margin-bottom: 3px;
margin-bottom: 6px;
}
.meta a {
.preview h3 {
background-color: #f0f3fc;
border: 0px solid #e6e6e6;
border-bottom-width: 1px;
color: black;
margin: 0px;
padding: 10px;
font-size: 14px;
}
.dark .preview h3 {
background-color: #333;
border-color: #333;
color: #ccc;
}
.preview .comment {
margin-top: 5px;
padding: 0px;
}
.preview .comment .content {
padding: 5px 10px;
}
.meta a, .activity .meta a {
color: #369;
text-decoration: none;
font-size: 10px;
@ -197,6 +249,13 @@ code {
.dark .meta a {
color: #6a98af;
}
.comment .meta a.distinguished.admin, .post.distinguished a.admin, .post.announcement a.admin {
background-color: #ff0011;
color: white;
font-weight: bold;
border-radius: 3px;
padding: 0px 2px;
}
.meta a:hover {
text-decoration: underline;
}
@ -207,29 +266,65 @@ code {
border-radius: 3px;
padding: 0px 2px;
}
.comment .meta a.distinguished {
background-color: #228822;
color: white;
font-weight: bold;
border-radius: 3px;
padding: 0px 2px;
}
.commentmenu {
font-size: 16px;
margin: 0px 0px 10px 10px;
margin: 0px 0px 10px 0px;
}
.commentmenu div {
border-top: 1px dotted gray;
font-size: 12px;
}
.savecomment {
margin: 10px;
form.savecomment {
margin: 0px 0px 10px 0px;
width: 500px;
}
.comment > .children > form.savecomment {
margin: 0px 0px 10px 20px;
}
.comment .children {
margin: 5px 0px 10px 15px;
}
.savecomment textarea {
width: 500px;
margin: 5px 0px;
width: 100%;
height: 100px;
}
.savecomment .upload label div {
display: inline-block;
border: 1px solid #ccc;
height: 20px;
line-height: 20px;
width: 32px;
position: relative;
background-color: #999;
color: #000;
text-align: center;
cursor: pointer;
}
.savecomment .upload input {
display: none;
}
.savecomment .right {
float:right;
}
.savecomment .right a {
line-height: 28px;
font-size: 10px;
}
.comment .meta a.minimize {
color: #369;
font-size: 10px;
margin-right: 2px;
}
.comment.hidden {
margin-bottom: 10px;
}
.comment.hidden .meta a {
color: gray;
font-weight: 400;
@ -238,11 +333,15 @@ code {
font-style: italic;
font-weight: 700;
}
.comment.hidden .content, .comment.hidden .children {
.comment.hidden .content, .comment.hidden .children, .comment.hidden .morecomments {
display: none;
}
.children .morecomments {
}
.morecomments {
margin: 10px 0px;
height: 20px;
clear: left;
margin: 0px 0px 10px 0px;
font-size: 10px;
}
.morecomments a {
@ -253,11 +352,11 @@ code {
.morecomments a:hover {
text-decoration: underline;
}
.member {
.member, .block {
display: inline-block;
margin-bottom:3px;
}
.member input {
.member input, .block input {
font-size: 10px;
font-weight: bold;
display: inline-block;
@ -269,10 +368,10 @@ code {
bottom: 1px;
cursor: pointer;
}
.join input {
.join input, .block input {
background-color: green;
}
.leave input {
.leave input, .block.unblock input {
background-color: #cf6165;
}
.pending input {
@ -281,9 +380,37 @@ code {
.left {
float: left;
}
span.nsfw {
color: #d10023;
font-size: 10px;
line-height: 14px;
border-radius:3px;
border: 1px solid #d10023;
padding: 0 4px;
display: inline-block;
font-weight: 400;
}
form.nsfw {
text-align: center;
margin: 50px auto;
width: 650px;
font-size: 18px;
}
form.nsfw div {
font-size: 40px;
background-color: #ff575b;
display: inline-block;
padding: 20px 10px;
border-radius: 50%;
font-weight: bold;
color: white;
}
.gray {
color: #808080;
}
.loading {
color: red !important;
}
.error {
color: red;
font-size: 13px;
@ -306,7 +433,7 @@ code {
top: 1px;
}
.pager {
margin: 10px;
margin: 20px 0px;
}
.pager a {
padding: 1px 4px;
@ -317,6 +444,21 @@ code {
text-decoration: none;
color: #369;
}
.pager.hidden {
display: none;
}
#loadmore {
display: none;
}
#loadmore, #end {
margin: 10px 0px;
}
#loadmore.show {
display: block;
}
#end {
visibility: hidden;
}
.buttons li {
display: inline;
}
@ -325,8 +467,11 @@ code {
font-size: 10px;
padding: 0;
}
.comment .textarea {
width: 100%;
}
.comment .buttons {
margin: 8px 0px 3px 0px;
margin: 3px 0px 0px 0px;
}
.comment.hidden .buttons {
display: none;
@ -336,12 +481,13 @@ code {
border-left: 2px solid #c5c1ad;
padding: 0 8px;
}
.buttons a, .buttons form input {
.buttons a, .buttons form input, .comment .buttons form input {
text-decoration: none;
color: #888;
padding-right: 4px;
display: inline-block;
margin-right: 5px !important;
}
.buttons a:hover, .title a:hover, .buttons input:hover {
.buttons a:hover, .title a:hover, .buttons form input:hover, .comment .buttons form input:hover {
text-decoration: underline;
}
.entry {
@ -381,18 +527,94 @@ code {
.expando-button:hover{
background-color: #466599;
}
.expando-button.hidden {
.expando-button.hidden, .children.hidden .comment, .children.hidden .morecomments {
display: none;
}
.hidechildren .show {
display: none;
}
.hidechildren.hidden .show {
display: inline;
}
.hidechildren.hidden .hide {
display: none;
}
.hidechildren span {
pointer-events: none;
}
.expando {
display: none;
max-width: 587px;
max-width: 870px;
margin-top: 5px;
position: relative;
color: #000;
}
#mycommunities, #settingspopup {
background-color: white;
border: 1px solid #888;
display: none;
position: absolute;
z-index: 100;
}
#mycommunities {
top: 17px;
padding: 5px 0px;
border-width: 0px 1px 1px 0px;
}
#mycommunities div {
margin: 0px 5px;
}
#mycommunities a:first-child {
text-align: right;
}
#mycommunities a {
text-decoration: none;
color: #369;
text-transform: uppercase;
font-size: 9px;
display: block;
padding: 0px 3px;
}
.dark #mycommunities a {
color: #8cb3d9;
}
.dark #mycommunities a:hover {
background-color: #3e3e3e;
}
#mycommunities a:hover {
background-color: #c7def7;
}
#settingspopup {
right: 10px;
top: 45px;
}
#settingspopup form {
margin: 0px;
}
.dark #settingspopup, .dark #mycommunities {
background-color: #262626;
}
#settingspopup.open, #mycommunities.open {
display: inline-block;
}
.expando.open{
display: block;
}
.expando img {
.expando .embed {
text-align: center;
}
.expando .image {
display: block;
overflow: hidden;
resize: both;
max-width: 578px;
margin: 0 auto;
background-repeat: no-repeat;
background-size: contain;
background-position: top left;
}
.expando .image img {
visibility: hidden;
max-width: 100%;
}
.expando .md {
@ -400,13 +622,21 @@ code {
border: 1px solid #369;
border-radius: 7px;
padding: 5px 10px;
margin: 5px 0px;
margin: 5px auto;
max-width: 578px;
font-size: 14px;
overflow: auto;
}
.expando .md img {
max-width: 100%;
}
.expando.open.showimage .md {
display: none;
}
.dark .expando .md {
background-color: #262626;
color: #ddd;
border-color: #666;
}
.expando p, .comment p, .message p {
margin-top: 0;
@ -442,24 +672,30 @@ code {
text-align: right;
}
.side {
display: none;
margin: 0 auto;
font-size: 12px;
width: 300px;
padding-right: 5px;
margin-bottom: 10px;
}
.side img, .md img{
max-width: 100%;
}
main {
position: relative;
margin: 0px 10px;
}
@media (min-width: 900px) {
.side {
display: block;
position: absolute;
top: 0;
right: 0;
}
main {
padding-right: 310px;
margin-left: 10px;
padding-right: 316px;
margin-right: 0px;
}
}
.side form {
@ -544,7 +780,7 @@ main {
color: white;
}
.dark .create a:hover{
color: #1496dc;
color: #0cbe30;
}
.dark .create input[type=submit],
.dark .search .query input,
@ -604,8 +840,8 @@ h1, h2 {
margin-left: 36px;
}
nav {
border-bottom: 1px solid #5f99cf;
background-color: #cee3f8;
border-bottom: 1px solid #00a846;
background-color: #9ad59b;
z-index: 99;
margin-bottom: 5px;
position: relative;
@ -640,16 +876,20 @@ nav .communities a.more {
font-weight: bold;
position: absolute;
right: 0;
margin: 0;
}
.dark nav .communities a.more {
background-color: #cccccc;
}
.orangered, .orangered b {
color: orangered !important;
}
nav a {
nav .communities a {
text-decoration: none;
color: black;
}
nav > a:hover {
nav .title a:hover {
text-decoration: underline;
}
@ -662,7 +902,7 @@ nav .title, nav > span {
font-variant: small-caps;
}
.dark nav .title, .dark nav > span {
color: #8cb3d9;;
color: #ececec;
}
nav a.title {
margin-left: 70px;
@ -693,32 +933,37 @@ nav .icon img {
height:100%;
}
nav .tabs {
overflow-x: auto;
}
nav ul {
white-space: nowrap;
list-style: none;
margin: 5px 2.5px 0px 2.5px;
padding: 0;
display: inline;
display: inline-block;
vertical-align: bottom;
}
nav li {
white-space: nowrap;
margin: 0px 1px;
padding: 0px;
display: inline;
display: inline-block;
margin-top: 5px;
font-size: 12px;
font-weight: 700;
}
nav ul a {
color: #369;
background-color: #eff7ff;
text-decoration: none;
color: #369;
padding: 2px 6px 0 6px;
}
.dark nav ul a {
background-color: #262626;
color: #6a98af;
color: #ddd;
}
.selected {
@ -728,20 +973,23 @@ nav ul a {
nav .selected a {
color: orangered;
background-color: white;
border: 1px solid #5f99cf;
border: 1px solid #00a846;
border-bottom: 1px solid white;
}
.dark nav .selected a {
color: #d25a32;
border-bottom: 1px solid #262626;
}
nav .right {
position: absolute;
right: 0px;
background-color: #EFF7FF;
background-color: #EFFFEF;
padding: 4px 6px;
line-height: 12px;
border-bottom-left-radius: 7px;
border-bottom-right-radius: 7px;
margin-right: 10px;
color: gray;
font-size: 10px;
z-index: 101;
@ -755,12 +1003,12 @@ nav .right a.mailbox {
top: 4px;
color: gray;
}
nav .right a, .right input[type=submit] {
nav .right a, nav .right input[type=submit] {
color: #369;
text-decoration: none;
}
.dark nav .right a, .dark .right input[type=submit]{
color: #8cb3d9;
.dark nav .right a, .dark nav .right input[type=submit]{
color: #dadada;
}
nav .right form, .comment form, form.link-btn {
display: inline-block;
@ -805,10 +1053,16 @@ nav .right form input, .comment .buttons input, form.link-btn input {
padding: 5px 10px;
margin: 5px 10px;
}
.dark .warning {
background-color: #544400;
}
.highlight {
background-color: #ffc;
padding-left: 5px;
}
.dark .highlight{
background-color: #4c4c4c;
}
form.create {
width: 520px;
@ -843,13 +1097,21 @@ form.create input[type=file], form.create select {
content: "*";
color: red;
}
.preferences {
margin: 20px;
}
.preferences div {
font-size: 13px;
margin: 10px;
}
.preferences div:last-child label {
text-align: left;
font-size: 10px;
}
.preferences label{
display: inline-block;
width: 100px;
width: 150px;
margin-right: 5px;
text-align: right;
}

View file

@ -1,26 +1,24 @@
function request(url, params, callback) {
function request(url, params, callback, errorcallback = function(){}) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
callback(xmlHttp.responseText);
if (xmlHttp.readyState != 4 ) { return }
if (xmlHttp.status == 200) {
return callback(xmlHttp.responseText);
}
errorcallback(xmlHttp.responseText);
}
var method = "GET"
if (params) method = "POST"
xmlHttp.open(method, url, true);
if (method = "POST")
xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xmlHttp.send(params);
}
function postClick(e) {
console.log(e)
e = e || window.event;
if (e.target.className.indexOf("expando-button") == -1) { return }
var targ = e.currentTarget || e.srcElement || e;
if (targ.nodeType == 3) targ = targ.parentNode;
var bdy = targ.getElementsByClassName("expando")[0]
var btn = targ.getElementsByClassName("expando-button")[0]
console.log(bdy.style.display)
console.log(bdy.style.display.indexOf("block"))
if (bdy.className.indexOf("open")>-1) {
bdy.className = 'expando';
btn.className = "expando-button"
@ -29,26 +27,42 @@ function postClick(e) {
bdy.className = 'expando open';
btn.className = "expando-button open"
var url = targ.getElementsByClassName("url")[0].href
if (id = parse_youtube(url)) {
targ.getElementsByClassName("embed")[0].innerHTML = youtube_iframe(id)
if (id = parseYoutube(url)) {
targ.getElementsByClassName("embed")[0].innerHTML = youtubeIframe(id)
}
}
}
function uptil (el, f) {
if (el) return f(el) ? el : uptil(el.parentNode, f)
}
function commentClick(e) {
e = e || window.event;
var targ = e.currentTarget || e.srcElement || e;
if (targ.nodeType == 3) targ = targ.parentNode;
if (e.target.name=="vote") {
if (e.target.name=="submit") {
e.preventDefault()
var form = e.target.parentNode
var form = uptil(e.target, function(el){ return el.tagName == "FORM" })
if (form) {
data = new FormData(form)
if (("c"+data.get("commentid")) != targ.id) { return }
params = new URLSearchParams(data).toString()
params += "&" + e.target.name + "=" + e.target.value
params += "&xhr=1"
request(targ.target, params, function(res){
data.set(e.target.name, e.target.value)
data.set("xhr", 1)
if (("c"+data.get("commentid")) == targ.id) {
targ.action = form.action
if (e.target.value == "preview") {
targ = form
}
console.log("ok")
} else if (("c"+data.get("parentid")) == targ.id) {
targ = form
} else { return }
e.target.disabled = "disabled"
request(targ.action || "", data,
function(res){
targ.outerHTML = res
setup()
},
function(res){
e.target.disabled = ""
})
}
return false
@ -67,8 +81,21 @@ function commentClick(e) {
}
return false
}
if ((e.target.className.indexOf("loadmore") != -1) ||
(e.target.className.indexOf("edit") != -1) ||
if (e.target.className.indexOf("hidechildren") != -1) {
if (e.target.getAttribute("for") != targ.id) { return }
e.preventDefault()
var btn = targ.getElementsByClassName("hidechildren")[0]
var children = targ.getElementsByClassName("children")[0]
if (children.className.indexOf("hidden") == -1) {
children.className = "children hidden"
btn.className = "hidechildren hidden"
} else {
children.className = "children"
btn.className = "hidechildren"
}
return false
}
if ((e.target.className.indexOf("edit") != -1) ||
(e.target.className.indexOf("source") != -1) ||
(e.target.className.indexOf("reply") != -1)) {
var id = targ.id
@ -76,47 +103,341 @@ function commentClick(e) {
e.preventDefault()
request(e.target.href+"&xhr",false, function(res){
targ.outerHTML = res
setup()
})
return false
}
if (e.target.className.indexOf("loadmore") != -1) {
var id = targ.id
if (e.target.getAttribute("for") != id) { return }
e.preventDefault()
var comments = targ.getElementsByClassName("comment")
var skip = []
for (var i = 0; i < comments.length; i++) {
skip.push(comments[i].id)
}
request(e.target.href+"&xhr",false, function(res){
var parent = e.target.parentNode
parent.innerHTML = res
parent.innerHTML = parent.getElementsByClassName("children")[0].innerHTML
var comments = parent.getElementsByClassName("comment")
for (var i = 0; i < skip.length; i++) {
for (var c = 0; c < comments.length; c++) {
if (skip[i] == comments[c].id) {
comments[c].remove()
}
}
}
parent.outerHTML = parent.innerHTML
setup()
})
return false
}
}
function loadMoreComments(e) {
e.preventDefault()
page = e.target.getAttribute("data-page")
var urlParams = new URLSearchParams(window.location.search);
urlParams.set("xhr", "1")
urlParams.set("page", page)
e.target.innerHTML = "loading"
e.target.className = "loading"
request(window.location.origin+window.location.pathname+"?"+urlParams.toString(), "",
function(res){
if (res.trim()) {
e.target.parentNode.outerHTML = res + '<div class="morecomments"><a id="lmc" href="" data-page="'+(parseInt(page)+1)+'">load more comments</a></div>'
setup()
} else {
e.target.parentNode.innerHTML = ""
}
}, function() {
e.target.innerHTML = "loading failed"
})
return false;
}
function loadMore(e) {
e.preventDefault()
page = e.target.getAttribute("data-page")
e.target.disabled="disabled"
e.target.value="loading"
var urlParams = new URLSearchParams(window.location.search);
urlParams.set("xhr", "1")
urlParams.set("page", page)
request(window.location.origin+window.location.pathname+"?"+urlParams.toString(), "",
function(res){
if (res.trim()) {
e.target.outerHTML = res + '<input id="loadmore" type="submit" data-page="'+(parseInt(page)+1)+'" value="load more">'
if (showimages = document.getElementById("showimages")) {
if (showimages.className == "selected") {
toggleImages(true)
}
}
var loadmore = document.getElementById("loadmore")
loadmore.className = "show"
loadmore.addEventListener("click", loadMore)
setup()
}
else {
e.target.outerHTML = '<input id="end" type="submit" value="" disabled>'
}
},
function(res) {
e.target.outerHTML = '<input id="loadmore" type="submit" data-page="'+parseInt(page)+'" value="loading failed">'
var loadmore = document.getElementById("loadmore")
loadmore.className = "show"
loadmore.addEventListener("click", loadMore)
}
)
return false;
}
function hideAllChildComments(e) {
e.preventDefault()
var comments = document.getElementsByClassName("comment")
if (e.target.innerHTML == "hide all child comments") {
e.target.innerHTML = "show all child comments"
} else {
e.target.innerHTML = "hide all child comments"
}
for (var i = 0; i < comments.length; i++) {
var comment = comments[i]
var btn = comment.getElementsByClassName("hidechildren")
if (!btn.length) { continue }
btn = btn[0]
if (btn.getAttribute("for") != comment.id) { continue }
var children = comment.getElementsByClassName("children")[0]
if (e.target.innerHTML == "show all child comments") {
children.className = "children hidden"
btn.className = "hidechildren hidden"
} else {
children.className = "children"
btn.className = "hidechildren"
}
}
return false
}
function formSubmit(e) {
e = e || window.event;
var targ = e.currentTarget || e.srcElement || e;
console.log(e)
e.preventDefault()
var data = new FormData(targ)
params = new URLSearchParams(data).toString()
params += "&" + e.submitter.name + "=" + e.submitter.value
params += "&xhr=1"
request(targ.target, params, function(res){
data.set(e.submitter.name, e.submitter.value)
data.set("xhr", "1")
e.submitter.disabled = "disabled"
request(targ.target, data,
function(res){
if (data.get("op") == "read_post") {
document.getElementById("p"+data.get("postid")).remove()
return
}
targ.outerHTML = res
setup()
},
function(res){
e.submitter.disabled = ""
}
)
return false
}
function toggleMyCommunities(e) {
e.preventDefault()
var mycommunities = document.getElementById("mycommunities")
if (mycommunities.className.indexOf("open") > -1) {
mycommunities.className = ""
return false
}
mycommunities.className = "open"
if (mycommunities.innerHTML == "") {
mycommunities.innerHTML = "<div>loading</div>"
request(e.target.href + "&xhr=1", "", function(res) {
mycommunities.innerHTML = '<div><a href="'+e.target.href+'">view all »</a>'
mycommunities.innerHTML += res
}, function() {
mycommunities.className = ""
})
}
return false
}
function openSettings(e) {
e.preventDefault()
var settings = document.getElementById("settingspopup")
if (settings.className == "open") {
settings.className = ""
return false
}
settings.className = "open"
request(e.target.href + "?xhr=1", "", function(res) {
settings.innerHTML = res
var options = document.getElementsByClassName("scripting")
for (var i = 0; i < options.length; i++) {
var input = options[i].getElementsByTagName('input')
if (!input.length) { continue }
if (localStorage.getItem(input[0].name) == "true") {
input[0].checked = "checked"
}
}
document.getElementById("settings").addEventListener("submit", saveSettings)
document.getElementById("closesettings").addEventListener("click", closeSettings)
})
return false
}
function parse_youtube(url){
function closeSettings(e) {
e.preventDefault()
var settings = document.getElementById("settingspopup")
settings.className = ""
return false
}
function saveSettings(e) {
e = e || window.event;
var targ = e.currentTarget || e.srcElement || e;
var data = new FormData(targ)
e.preventDefault()
request(targ.target, data, function(res) {
["endlessScrolling", "autoLoad"].map(function(x) {
localStorage.setItem(x, data.get(x)=="on")
})
window.location.reload()
})
return false;
}
function parseYoutube(url){
if (url.indexOf("youtu") == -1) return false
var regExp = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/|shorts\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/;
var match = url.match(regExp);
if (match.length > 1) {
if (match && match.length > 1) {
return match[1]
}
return false
}
function youtube_iframe(id) {
function youtubeIframe(id) {
return '<iframe width="560" height="315" src="https://www.youtube.com/embed/'+id+'" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>'
}
var posts = document.getElementsByClassName("post")
for (var i = 0; i < posts.length; i++) {
var url = posts[i].getElementsByClassName("url")[0].href
if (id = parse_youtube(url)) {
var btn = posts[i].getElementsByClassName("expando-button")[0]
if (btn.className.indexOf("open") > -1) {
console.log(id)
posts[i].getElementsByClassName("embed")[0].innerHTML = youtube_iframe(id)
function showImages(e) {
e = e || window.event;
e.preventDefault()
var targ = e.currentTarget || e.srcElement || e;
var parent = targ.parentNode
if (parent.className == "") {
parent.className = "selected"
toggleImages(true)
} else {
parent.className = ""
toggleImages(false)
}
return false
}
function toggleImages(open) {
var posts = document.getElementsByClassName("post")
for (var i = 0; i < posts.length; i++) {
var btn = posts[i].getElementsByClassName("expando-button")[0]
if (btn.className.indexOf("hidden") != -1) { continue }
var img = posts[i].getElementsByClassName("image")
if (!img.length) { continue }
var bdy = posts[i].getElementsByClassName("expando")[0]
if (open) {
bdy.className = 'expando open showimage';
btn.className = "expando-button open"
} else {
bdy.className = 'expando';
btn.className = "expando-button"
}
}
}
function insertImg(e) {
e = e || window.event;
var form = uptil(e.target, function(el){ return el.tagName == "FORM" })
form.querySelector("input[value=preview]").click()
var inputs = form.getElementsByTagName("input")
for (var i = 0; i < inputs.length; i++) {
inputs[i].disabled = "disabled"
}
}
function setup() {
if (showimages = document.getElementById("se")) {
showimages.addEventListener("click", showImages)
}
if (settings = document.getElementById("opensettings")) {
settings.addEventListener("click", openSettings)
}
if (settings = document.getElementById("openmycommunities")) {
settings.addEventListener("click", toggleMyCommunities)
}
if (hidechildren = document.getElementById("hidechildren")){
hidechildren.addEventListener("click", hideAllChildComments)
}
if (lmc = document.getElementById("lmc")){
var pager = document.getElementsByClassName("pager")
if (pager.length) {
pager[0].style.display = "none";
}
lmc.addEventListener("click", loadMoreComments)
}
var imgUpload = document.getElementsByClassName("imgupload")
for (var i = 0; i < imgUpload.length; i++) {
imgUpload[i].addEventListener("change", insertImg)
}
var posts = document.getElementsByClassName("post")
for (var i = 0; i < posts.length; i++) {
posts[i].addEventListener("click", postClick)
var forms = posts[i].getElementsByClassName("link-btn")
for (var f = 0; f < forms.length; f++) {
forms[f].addEventListener("submit", formSubmit)
}
var url = posts[i].getElementsByClassName("url")[0].href
if (id = parseYoutube(url)) {
var btn = posts[i].getElementsByClassName("expando-button")[0]
if (btn.className.indexOf("open") > -1) {
posts[i].getElementsByClassName("embed")[0].innerHTML = youtubeIframe(id)
} else {
btn.className = "expando-button"
}
}
}
var comments = document.getElementsByClassName("comment")
for (var i = 0; i < comments.length; i++) {
comments[i].addEventListener("click", commentClick)
}
}
setup()
if (localStorage.getItem("endlessScrolling") == "true") {
var pager = document.getElementsByClassName("pager")
if (pager.length) pager[0].className = "pager hidden"
var loadmore = document.getElementById("loadmore")
if (loadmore) {
loadmore.className = "show"
loadmore.addEventListener("click", loadMore)
}
}
if (localStorage.getItem("autoLoad") == "true") {
window.onscroll = function(e) {
if ((window.innerHeight + Math.round(window.scrollY)) >= document.body.offsetHeight) {
if (localStorage.getItem("endlessScrolling") == "true") {
if (loadmore = document.getElementById("loadmore")) {
loadmore.click()
}
}
if (lmc = document.getElementById("lmc")) {
lmc.click()
}
}
};
}
// delete cookies without HTTPOnly
var cookies = document.cookie.split(";");
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i];
var eqPos = cookie.indexOf("=");
var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;SameSite=None;Secure";
}

33
public/ws.js Normal file
View file

@ -0,0 +1,33 @@
var ok = false
var t = 800
var ws
var prot = location.protocol == 'https:' ? "wss://":"ws://"
let port = ":8009"
function start(){
let url = prot + document.location.hostname + port + document.location.pathname + document.location.search;
console.log('connecting to ', url);
ws = new WebSocket(url);
ws.onopen = function(){
console.log("open");
t = 800
}
ws.onmessage = function(msg){
console.log("reload:", msg.data)
if (msg.data == "") {
return
}
window.location.reload()
}
ws.onclose = function(){
console.log("close");
setTimeout(function(){
//start()
if (t < 10 * 1000) t += 200
}, t);
};
}
console.log("ws");
if (typeof WebSocket != 'undefined') {
start()
}

465
routes.go
View file

@ -18,6 +18,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/julienschmidt/httprouter"
"github.com/k3a/html2text"
"github.com/rystaf/go-lemmy"
"github.com/rystaf/go-lemmy/types"
"golang.org/x/text/language"
@ -31,12 +32,12 @@ var funcMap = template.FuncMap{
}
return host
},
"proxy": func(s string) string {
"localize": func(s string) string {
u, err := url.Parse(s)
if err != nil {
return s
}
return "/" + u.Host + u.Path
return "." + u.Path + "@" + u.Host
},
"printer": func(n any) string {
p := message.NewPrinter(language.English)
@ -110,24 +111,109 @@ var funcMap = template.FuncMap{
}
return false
},
"thumbnail": func(p types.Post) string {
if p.ThumbnailURL.IsValid() {
return p.ThumbnailURL.String() + "?format=jpg&thumbnail=96"
}
re := regexp.MustCompile(`\/pictrs\/image\/([a-z0-9\-]+)\.([a-z]+)$`)
if re.MatchString(p.URL.String()) {
return p.URL.String() + "?format=jpg&thumbnail=96"
}
re = regexp.MustCompile(`^https:\/\/(i\.)?imgur.com\/([a-zA-Z0-9]{5,})(\.[a-zA-Z0-9]+)?`)
if re.MatchString(p.URL.String()) {
return re.ReplaceAllString(p.URL.String(), "https://i.imgur.com/${2}s.jpg")
}
if p.URL.IsValid() {
return "/_/static/link.png"
}
return "/_/static/text.png"
},
"humanize": humanize.Time,
"markdown": func(host string, body string) template.HTML {
var buf bytes.Buffer
re := regexp.MustCompile(`\s---\s`)
body = re.ReplaceAllString(body, "\n***\n")
// community bangs
body = RegReplace(body, `([^\[])!([a-zA-Z0-9_]+)@([a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)+)`, `$1[!$2@$3](/c/$2@$3)`)
if err := md.Convert([]byte(body), &buf); err != nil {
panic(err)
fmt.Println(err)
return template.HTML(body)
}
converted := buf.String()
converted = strings.Replace(converted, `<img `, `<img loading="lazy" `, -1)
if os.Getenv("LEMMY_DOMAIN") != "" {
re := regexp.MustCompile(`href="https:\/\/([a-zA-Z0-9\.]+\/(c\/[a-zA-Z0-9]+|(post|comment)\/\d+))`)
converted = re.ReplaceAllString(converted, `href="/$1`)
body = buf.String()
body = strings.Replace(body, `<img `, `<img loading="lazy" `, -1)
body = LemmyLinkRewrite(body, host, os.Getenv("LEMMY_DOMAIN"))
body = RegReplace(body, `::: ?spoiler (.*?)\n([\S\s]*?):::`, "<details><summary>$1</summary>$2</details>")
return template.HTML(body)
},
"rmmarkdown": func(body string) string {
var buf bytes.Buffer
if err := md.Convert([]byte(body), &buf); err != nil {
fmt.Println(err)
return body
}
return template.HTML(converted)
text := html2text.HTML2TextWithOptions(buf.String(), html2text.WithLinksInnerText())
re := regexp.MustCompile(`\<(https?:\/\/|mailto)(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)\>`)
return re.ReplaceAllString(text, "")
},
"contains": strings.Contains,
"sub": func(a int32, b int) int {
return int(a) - b
},
"add": func(a int32, b int) int {
return int(a) + b
},
}
func LemmyLinkRewrite(input string, host string, lemmy_domain string) (body string) {
body = input
// localize community and user links
body = RegReplace(body, `href="https:\/\/([a-zA-Z0-9\.\-]+)\/((c|u|comment|post)\/[^#\?]*?)"`, `href="/$2@$1"`)
// remove extra instance tag
body = RegReplace(body, `href="(https:\/)?(\/[a-zA-Z0-9\.\-]+)?\/((c|u)\/[a-zA-Z0-9]+@[a-zA-Z0-9\.\-]+)@([a-zA-Z0-9\.\-]+)"`, `href="/$3"`)
if lemmy_domain == "" {
// add domain to relative links
body = RegReplace(body, `href="\/((c|u|post|comment)\/(.*?)")`, `href="/`+host+`/$1`)
// convert links to relative
body = RegReplace(body, `href="https:\/\/([a-zA-Z0-9\.\-]+\/((c|u|post|comment)\/[a-zA-Z0-9]+"))`, `href="/$1`)
} else {
// convert local links to relative
body = RegReplace(body, `href="https:\/\/`+lemmy_domain+`\/(c\/[a-zA-Z0-9]+"|(c|u|post|comment)\/(.*?)")`, `href="/$1`)
body = RegReplace(body, `href="(.*)@`+lemmy_domain+`"`, `href="$1"`)
}
re := regexp.MustCompile(`href="\/?([a-zA-Z0-9\.\-]*)\/(c|u|post|comment)\/(.*?)@(.*?)"`)
// assume "old." subdomain is mlmym and remove
matches := re.FindAllStringSubmatch(body, -1)
for _, match := range matches {
if match[4][0:4] == "old." {
s := 1
if match[1] == "" {
s += 1
}
body = strings.Replace(body, match[0], `href="/`+strings.Join(match[s:4], "/")+"@"+match[4][4:]+`"`, -1)
}
}
// remove redundant instance tag
matches = re.FindAllStringSubmatch(body, -1)
for _, match := range matches {
if match[1] == match[4] {
body = strings.Replace(body, match[0], `href="/`+strings.Join(match[1:4], "/")+`"`, -1)
}
}
return body
}
func RegReplace(input string, match string, replace string) string {
re := regexp.MustCompile(match)
return re.ReplaceAllString(input, replace)
}
func getenv(key, fallback string) string {
value := os.Getenv(key)
if len(value) == 0 {
return fallback
}
return value
}
func Initialize(Host string, r *http.Request) (State, error) {
@ -135,6 +221,10 @@ func Initialize(Host string, r *http.Request) (State, error) {
Host: Host,
Page: 1,
Status: http.StatusOK,
Version: version,
}
if watch != nil {
state.Watch = *watch
}
lemmyDomain := os.Getenv("LEMMY_DOMAIN")
if lemmyDomain != "" {
@ -157,7 +247,7 @@ func Initialize(Host string, r *http.Request) (State, error) {
token := getCookie(r, "jwt")
user := getCookie(r, "user")
parts := strings.Split(user, ":")
if len(parts) == 2 {
if len(parts) == 2 && token != "" {
if id, err := strconv.Atoi(parts[1]); err == nil {
state.Client.Token = token
sess := Session{
@ -169,13 +259,28 @@ func Initialize(Host string, r *http.Request) (State, error) {
}
state.Listing = getCookie(r, "DefaultListingType")
state.Sort = getCookie(r, "DefaultSortType")
state.Dark = getCookie(r, "Dark") != ""
state.CommentSort = getCookie(r, "DefaultCommentSortType")
if dark := getCookie(r, "Dark"); dark != "" {
state.Dark = dark != "0"
} else {
state.Dark = os.Getenv("DARK") != ""
}
state.ShowNSFW = getCookie(r, "ShowNSFW") != ""
state.HideInstanceNames = getCookie(r, "HideInstanceNames") != ""
if hide := getCookie(r, "HideThumbnails"); hide != "" {
state.HideThumbnails = hide != "0"
} else {
state.HideThumbnails = os.Getenv("HIDE_THUMBNAILS") != ""
}
state.ParseQuery(r.URL.RawQuery)
if state.Sort == "" {
state.Sort = "Hot"
state.Sort = getenv("SORT", "Hot")
}
if state.CommentSort == "" {
state.CommentSort = getenv("COMMENT_SORT", "Hot")
}
if state.Listing == "" || state.Session == nil && state.Listing == "Subscribed" {
state.Listing = "All"
state.Listing = getenv("LISTING", "All")
}
return state, nil
}
@ -210,6 +315,8 @@ func Render(w http.ResponseWriter, templateName string, state State) {
if state.Status != http.StatusOK {
w.WriteHeader(state.Status)
}
header := w.Header()
header.Set("Content-Security-Policy", "script-src 'self'")
err = tmpl.Execute(w, state)
if err != nil {
fmt.Println("execute fail", err)
@ -219,6 +326,7 @@ func Render(w http.ResponseWriter, templateName string, state State) {
}
func GetRoot(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
data := make(map[string]any)
data["Title"] = r.Host
tmpl, err := GetTemplate("root.html")
if err != nil {
fmt.Println("execute fail", err)
@ -334,7 +442,49 @@ func GetFrontpage(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
if state.Op == "" {
state.GetPosts()
}
if state.XHR {
Render(w, "xhr.html", state)
} else {
Render(w, "frontpage.html", state)
}
}
func GetCommunities(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
r.URL.Path = "/search"
if ps.ByName("host") != "" {
r.URL.Path = "/" + ps.ByName("host") + "/search"
}
r.URL.RawQuery = "searchtype=Communities&sort=TopMonth"
http.Redirect(w, r, r.URL.String(), 301)
}
func ResolveId(r *http.Request, class string, id string, host string) string {
remoteAddr := r.RemoteAddr
if r.Header.Get("CF-Connecting-IP") != "" {
remoteAddr = r.Header.Get("CF-Connecting-IP")
}
client := http.Client{Transport: NewAddHeaderTransport(remoteAddr)}
c, err := lemmy.NewWithClient("https://"+host, &client)
if err != nil {
return ""
}
idn, _ := strconv.Atoi(id)
if class == "post" {
resp, err := c.Post(context.Background(), types.GetPost{
ID: types.NewOptional(idn),
})
if err != nil {
return ""
}
return resp.PostView.Post.ApID
}
resp, err := c.Comment(context.Background(), types.GetComment{
ID: idn,
})
if err != nil {
return ""
}
return resp.CommentView.Comment.ApID
}
func GetPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
@ -343,11 +493,40 @@ func GetPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
Render(w, "index.html", state)
return
}
if path := strings.Split(ps.ByName("postid"), "@"); len(path) > 1 {
apid := ResolveId(r, "post", path[0], path[1])
if apid != "" {
resp, err := state.Client.ResolveObject(context.Background(), types.ResolveObject{
Q: apid,
})
if err != nil {
dest := apid
if os.Getenv("LEMMY_DOMAIN") == "" {
dest = RegReplace(dest, `https:\/\/([a-zA-Z0-9\.\-]+\/post\/\d+)`, `/$1`)
}
http.Redirect(w, r, dest, 302)
return
}
post, _ := resp.Post.Value()
if post.Post.ID > 0 {
dest := RegReplace(r.URL.String(), `(([a-zA-Z0-9\.\-]+)?/post/)([a-zA-Z0-9\-\.@]+)`, `$1`)
dest += strconv.Itoa(post.Post.ID)
http.Redirect(w, r, dest, 302)
return
} else {
http.Redirect(w, r, apid, 302)
return
}
}
}
m, _ := url.ParseQuery(r.URL.RawQuery)
if len(m["edit"]) > 0 {
state.Op = "edit_post"
state.GetSite()
}
if len(m["content"]) > 0 {
state.Content = m["content"][0]
}
postid, _ := strconv.Atoi(ps.ByName("postid"))
state.GetPost(postid)
state.GetComments()
@ -359,6 +538,32 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
Render(w, "index.html", state)
return
}
if path := strings.Split(ps.ByName("commentid"), "@"); len(path) > 1 {
apid := ResolveId(r, "comment", path[0], path[1])
if apid != "" {
resp, err := state.Client.ResolveObject(context.Background(), types.ResolveObject{
Q: apid,
})
if err != nil {
dest := apid
if os.Getenv("LEMMY_DOMAIN") == "" {
dest = RegReplace(dest, `https:\/\/([a-zA-Z0-9\.\-]+\/comment\/\d+)`, `/$1`)
}
http.Redirect(w, r, dest, 302)
return
}
comment, _ := resp.Comment.Value()
if comment.Comment.ID > 0 {
dest := RegReplace(r.URL.String(), `(([a-zA-Z0-9\.\-]+)?/comment/)([a-zA-Z0-9\-\.@]+)`, `$1`)
dest += strconv.Itoa(comment.Comment.ID)
http.Redirect(w, r, dest, 302)
return
} else {
http.Redirect(w, r, apid, 302)
return
}
}
}
m, _ := url.ParseQuery(r.URL.RawQuery)
if len(m["reply"]) > 0 {
state.Op = "reply"
@ -366,11 +571,22 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if len(m["edit"]) > 0 {
state.Op = "edit"
}
if r.Method == "POST" && len(m["content"]) > 0 {
state.Content = m["content"][0]
}
if len(m["source"]) > 0 {
state.Op = "source"
}
if len(m["context"]) > 0 {
ctx, _ := strconv.Atoi(m["context"][0])
state.Context = ctx
}
commentid, _ := strconv.Atoi(ps.ByName("commentid"))
state.GetComment(commentid)
if state.XHR && len(m["content"]) > 0 {
Render(w, "create_comment.html", state)
return
}
state.GetPost(state.PostID)
Render(w, "index.html", state)
}
@ -464,6 +680,9 @@ func setCookie(w http.ResponseWriter, host string, name string, value string) {
Name: name,
Value: value,
MaxAge: 86400 * 30,
HttpOnly: true,
SameSite: http.SameSiteNoneMode,
Secure: true,
Path: "/" + host,
}
http.SetCookie(w, &cookie)
@ -485,23 +704,49 @@ func Settings(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
Render(w, "index.html", state)
return
}
state.GetSite()
switch r.Method {
case "POST":
for _, name := range []string{"DefaultSortType", "DefaultListingType"} {
setCookie(w, state.Host, name, r.FormValue(name))
for _, name := range []string{"DefaultSortType", "DefaultListingType", "DefaultCommentSortType"} {
deleteCookie(w, state.Host, name)
setCookie(w, "", name, r.FormValue(name))
}
if r.FormValue("darkmode") != "" {
setCookie(w, state.Host, "Dark", "1")
setCookie(w, "", "Dark", "1")
state.Dark = true
} else {
deleteCookie(w, state.Host, "Dark")
setCookie(w, "", "Dark", "0")
state.Dark = false
}
if r.FormValue("shownsfw") != "" {
setCookie(w, "", "ShowNSFW", "1")
state.ShowNSFW = true
} else {
deleteCookie(w, state.Host, "ShowNSFW")
deleteCookie(w, "", "ShowNSFW")
state.ShowNSFW = false
}
if r.FormValue("hideInstanceNames") != "" {
setCookie(w, "", "HideInstanceNames", "1")
state.HideInstanceNames = true
} else {
deleteCookie(w, "", "HideInstanceNames")
state.HideInstanceNames = false
}
if r.FormValue("hideThumbnails") != "" {
setCookie(w, "", "HideThumbnails", "1")
state.HideInstanceNames = true
} else {
setCookie(w, "", "HideThumbnails", "0")
state.HideInstanceNames = false
}
state.Listing = r.FormValue("DefaultListingType")
state.Sort = r.FormValue("DefaultSortType")
state.CommentSort = r.FormValue("DefaultCommentSortType")
// TODO save user settings
case "GET":
if state.Session != nil {
// TODO fetch server settings
// TODO fetch user settings
}
}
Render(w, "settings.html", state)
@ -540,6 +785,7 @@ func SignUpOrLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
if resp.JWT.IsValid() {
token = resp.JWT.String()
username = r.FormValue("username")
deleteCookie(w, state.Host, "ShowNSFW")
}
case "sign up":
register := types.Register{
@ -583,10 +829,14 @@ func SignUpOrLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
q.Add("alert", alert)
r.URL.RawQuery = q.Encode()
http.Redirect(w, r, r.URL.String(), 301)
return
}
}
if token != "" {
state.GetUser(username)
if state.User == nil {
return
}
setCookie(w, state.Host, "jwt", token)
userid := strconv.Itoa(state.User.PersonView.Person.ID)
setCookie(w, state.Host, "user", state.User.PersonView.Person.Name+":"+userid)
@ -603,7 +853,7 @@ func GetLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
return
}
state.GetSite()
if state.Site.SiteView.LocalSite.CaptchaEnabled {
if state.Site != nil && state.Site.SiteView.LocalSite.CaptchaEnabled {
state.GetCaptcha()
}
m, _ := url.ParseQuery(r.URL.RawQuery)
@ -669,6 +919,18 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
CommunityID: communityid,
Follow: true,
})
case "block":
communityid, _ := strconv.Atoi(r.FormValue("communityid"))
state.Client.BlockCommunity(context.Background(), types.BlockCommunity{
CommunityID: communityid,
Block: true,
})
case "unblock":
communityid, _ := strconv.Atoi(r.FormValue("communityid"))
state.Client.BlockCommunity(context.Background(), types.BlockCommunity{
CommunityID: communityid,
Block: false,
})
case "logout":
deleteCookie(w, state.Host, "jwt")
deleteCookie(w, state.Host, "user")
@ -684,10 +946,15 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if err != nil {
if strings.Contains(fmt.Sprintf("%v", err), "missing_totp_token") {
state.Op = "2fa"
Render(w, "login.html", state)
return
}
state.GetSite()
if state.Site != nil && state.Site.SiteView.LocalSite.CaptchaEnabled {
state.GetCaptcha()
}
state.Status = http.StatusUnauthorized
state.Error = err
Render(w, "login.html", state)
return
} else if resp.JWT.IsValid() {
state.GetUser(r.FormValue("username"))
if state.User != nil {
@ -820,6 +1087,7 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
post := types.EditPost{
PostID: postid,
Body: types.NewOptional(r.FormValue("body")),
Name: types.NewOptional(r.FormValue("name")),
URL: types.NewOptional(r.FormValue("url")),
}
if r.FormValue("url") == "" {
@ -850,6 +1118,38 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
state.Error = err
fmt.Println(err)
}
case "save_post":
postid, _ := strconv.Atoi(r.FormValue("postid"))
_, err := state.Client.SavePost(context.Background(), types.SavePost{
PostID: postid,
Save: r.FormValue("submit") == "save",
})
if err != nil {
fmt.Println(err)
}
if r.FormValue("xhr") != "" {
state.GetPost(postid)
state.PostID = 0
state.Op = "save_post"
state.XHR = true
Render(w, "index.html", state)
return
}
case "save_comment":
commentid, _ := strconv.Atoi(r.FormValue("commentid"))
_, err := state.Client.SaveComment(context.Background(), types.SaveComment{
CommentID: commentid,
Save: r.FormValue("submit") == "save",
})
if err != nil {
fmt.Println(err)
}
if r.FormValue("xhr") != "" {
state.XHR = true
state.GetComment(commentid)
Render(w, "index.html", state)
return
}
case "delete_post":
postid, _ := strconv.Atoi(r.FormValue("postid"))
post := types.DeletePost{
@ -866,6 +1166,22 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
r.URL.Path = "/" + state.Host + "/c/" + resp.PostView.Community.Name
r.URL.RawQuery = ""
}
case "read_post":
postid, _ := strconv.Atoi(r.FormValue("postid"))
post := types.MarkPostAsRead{
PostID: postid,
Read: true,
}
if r.FormValue("submit") == "mark unread" {
post.Read = false
}
_, err := state.Client.MarkPostAsRead(context.Background(), post)
if err != nil {
fmt.Println(err)
} else if r.FormValue("xhr") != "" {
w.Write([]byte{})
return
}
case "vote_post":
var score int16
score = 1
@ -883,6 +1199,8 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
state.Client.CreatePostLike(context.Background(), post)
if r.FormValue("xhr") != "" {
state.GetPost(postid)
state.PostID = 0
state.Op = "vote_post"
state.XHR = true
Render(w, "index.html", state)
return
@ -890,7 +1208,7 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
case "vote_comment":
var score int16
score = 1
if r.FormValue("vote") != "▲" {
if r.FormValue("submit") != "▲" {
score = -1
}
if r.FormValue("undo") == strconv.Itoa(int(score)) {
@ -917,8 +1235,20 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
parentid, _ := strconv.Atoi(r.FormValue("parentid"))
state.GetComment(parentid)
}
content := r.FormValue("content")
file, handler, err := r.FormFile("file")
if err == nil {
pres, err := state.UploadImage(file, handler)
if err != nil {
state.Error = err
Render(w, "index.html", state)
return
}
content += ("![](https://" + state.Host + "/pictrs/image/" + pres.Files[0].Filename + ")")
}
if r.FormValue("submit") == "save" {
createComment := types.CreateComment{
Content: r.FormValue("content"),
Content: content,
PostID: state.PostID,
}
if state.CommentID > 0 {
@ -926,6 +1256,13 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
}
resp, err := state.Client.CreateComment(context.Background(), createComment)
if err == nil {
if r.FormValue("xhr") != "" {
state.XHR = true
state.Comments = nil
state.GetComment(resp.CommentView.Comment.ID)
Render(w, "index.html", state)
return
}
postid := strconv.Itoa(state.PostID)
commentid := strconv.Itoa(resp.CommentView.Comment.ID)
r.URL.Path = "/" + state.Host + "/post/" + postid
@ -933,11 +1270,48 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
} else {
fmt.Println(err)
}
} else if r.FormValue("submit") == "preview" {
q := r.URL.Query()
q.Set("content", content)
q.Set("reply", "")
if r.FormValue("xhr") != "" {
q.Set("xhr", "1")
}
r.URL.RawQuery = q.Encode()
if ps.ByName("postid") != "" {
GetPost(w, r, ps)
return
}
if ps.ByName("commentid") != "" {
GetComment(w, r, ps)
return
}
} else if r.FormValue("xhr") != "" {
w.Write([]byte{})
return
}
if r.FormValue("submit") == "cancel" {
r.URL.RawQuery = ""
}
case "edit_comment":
commentid, _ := strconv.Atoi(r.FormValue("commentid"))
q := r.URL.Query()
content := r.FormValue("content")
file, handler, err := r.FormFile("file")
if err == nil {
pres, err := state.UploadImage(file, handler)
if err != nil {
state.Error = err
Render(w, "index.html", state)
return
}
content += ("![](https://" + state.Host + "/pictrs/image/" + pres.Files[0].Filename + ")")
}
if r.FormValue("submit") == "save" {
resp, err := state.Client.EditComment(context.Background(), types.EditComment{
CommentID: commentid,
Content: types.NewOptional(r.FormValue("content")),
Content: types.NewOptional(content),
})
if err != nil {
fmt.Println(err)
@ -946,6 +1320,39 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
r.URL.Fragment = "c" + commentid
r.URL.RawQuery = ""
}
} else if r.FormValue("submit") == "preview" {
q.Set("content", content)
q.Set("edit", "")
if r.FormValue("xhr") != "" {
q.Set("xhr", "1")
}
r.URL.RawQuery = q.Encode()
if ps.ByName("commentid") != "" {
GetComment(w, r, ps)
return
}
} else if r.FormValue("submit") == "cancel" {
if ps.ByName("commentid") != "" {
if r.FormValue("xhr") != "" {
q.Set("xhr", "1")
}
r.URL.RawQuery = q.Encode()
GetComment(w, r, ps)
return
}
} else if r.FormValue("xhr") != "" {
w.Write([]byte{})
return
}
if r.FormValue("xhr") != "" {
state.XHR = true
state.GetComment(commentid)
Render(w, "index.html", state)
return
}
if r.FormValue("submit") == "cancel" {
r.URL.RawQuery = ""
}
case "delete_comment":
commentid, _ := strconv.Atoi(r.FormValue("commentid"))
resp, err := state.Client.DeleteComment(context.Background(), types.DeleteComment{
@ -959,6 +1366,12 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
r.URL.Fragment = "c" + commentid
r.URL.RawQuery = ""
}
case "shownsfw":
if r.FormValue("submit") == "continue" {
setCookie(w, "", "ShowNSFW", "1")
} else {
r.URL.Path = "/" + state.Host
}
}
http.Redirect(w, r, r.URL.String(), 301)
}
@ -973,6 +1386,7 @@ func GetRouter() *httprouter.Router {
router.GET("/:host/search", middleware(Search))
router.POST("/:host/search", middleware(UserOp))
router.GET("/:host/inbox", middleware(Inbox))
router.POST("/:host/inbox", middleware(UserOp))
router.GET("/:host/login", middleware(GetLogin))
router.POST("/:host/login", middleware(SignUpOrLogin))
router.GET("/:host/settings", middleware(Settings))
@ -997,12 +1411,14 @@ func GetRouter() *httprouter.Router {
router.POST("/:host/create_post", middleware(UserOp))
router.GET("/:host/create_community", middleware(GetCreateCommunity))
router.POST("/:host/create_community", middleware(UserOp))
router.GET("/:host/communities", middleware(GetCommunities))
} else {
router.ServeFiles("/_/static/*filepath", http.Dir("public"))
router.GET("/", middleware(GetFrontpage))
router.GET("/search", middleware(Search))
router.POST("/search", middleware(UserOp))
router.GET("/inbox", middleware(Inbox))
router.POST("/inbox", middleware(UserOp))
router.GET("/login", middleware(GetLogin))
router.POST("/login", middleware(SignUpOrLogin))
router.GET("/settings", middleware(Settings))
@ -1027,6 +1443,7 @@ func GetRouter() *httprouter.Router {
router.POST("/create_post", middleware(UserOp))
router.GET("/create_community", middleware(GetCreateCommunity))
router.POST("/create_community", middleware(UserOp))
router.GET("/communities", middleware(GetCommunities))
}
return router
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

BIN
screenshot1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

150
state.go
View file

@ -11,6 +11,8 @@ import (
"mime/multipart"
"net/http"
"net/url"
"os"
"regexp"
"sort"
"strconv"
"strings"
@ -33,6 +35,12 @@ func (c *Comment) Submitter() bool {
return c.P.Comment.CreatorID == c.P.Post.CreatorID
}
func (c *Comment) ParentID() int {
path := strings.Split(c.P.Comment.Path, ".")
id, _ := strconv.Atoi(path[len(path)-2])
return id
}
type Person struct {
types.PersonViewSafe
}
@ -53,9 +61,12 @@ type Post struct {
type Session struct {
UserName string
UserID int
Communities []types.CommunityView
}
type State struct {
Watch bool
Version string
Client *lemmy.Client
HTTPClient *http.Client
Session *Session
@ -69,6 +80,7 @@ type State struct {
Communities []types.CommunityView
UnreadCount int64
Sort string
CommentSort string
Listing string
Page int
Parts []string
@ -78,6 +90,7 @@ type State struct {
CommentCount int
PostID int
CommentID int
Context int
UserName string
User *types.GetPersonDetailsResponse
Now int64
@ -85,11 +98,46 @@ type State struct {
Op string
Site *types.GetSiteResponse
Query string
Content string
SearchType string
Captcha *types.CaptchaResponse
Dark bool
ShowNSFW bool
HideInstanceNames bool
HideThumbnails bool
}
func (s State) Unknown() string {
fmt.Println(fmt.Sprintf("%v", s.Error))
re := regexp.MustCompile(`(.*?)@(.*?)@`)
if strings.Contains(fmt.Sprintf("%v", s.Error), "couldnt_find_community") {
matches := re.FindAllStringSubmatch(s.CommunityName+"@", -1)
if len(matches) < 1 || len(matches[0]) < 3 {
return ""
}
if matches[0][2] != s.Host {
remote := "/" + matches[0][2] + "/c/" + matches[0][1]
if os.Getenv("LEMMY_DOMAIN") != "" {
remote = "https:/" + remote
}
return remote
}
}
if strings.Contains(fmt.Sprintf("%v", s.Error), "couldnt_find_that_username_or_email") {
matches := re.FindAllStringSubmatch(s.UserName+"@", -1)
if len(matches) < 1 || len(matches[0]) < 3 {
return ""
}
if matches[0][2] != s.Host {
remote := "/" + matches[0][2] + "/u/" + matches[0][1]
if os.Getenv("LEMMY_DOMAIN") != "" {
remote = "https:/" + remote
}
return remote
}
}
return ""
}
func (p State) SortBy(v string) string {
var q string
if p.Query != "" || p.SearchType == "Communities" {
@ -155,6 +203,7 @@ func (state *State) ParseQuery(RawQuery string) {
}
if len(m["sort"]) > 0 {
state.Sort = m["sort"][0]
state.CommentSort = m["sort"][0]
}
if len(m["communityname"]) > 0 {
state.CommunityName = m["communityname"][0]
@ -168,6 +217,11 @@ func (state *State) ParseQuery(RawQuery string) {
if len(m["xhr"]) > 0 {
state.XHR = true
}
if len(m["view"]) > 0 {
if m["view"][0] == "Saved" {
state.Op = "Saved"
}
}
//if len(m["op"]) > 0 {
// state.Op = m["op"][0]
//}
@ -208,16 +262,27 @@ func (state *State) GetCaptcha() {
}
}
func (state *State) GetSite() {
token := state.Client.Token
state.Client.Token = ""
resp, err := state.Client.Site(context.Background(), types.GetSite{})
if err != nil {
fmt.Println(err)
state.Status = http.StatusInternalServerError
state.Host = ""
state.Host = "."
state.Error = errors.New("unable to retrieve site")
return
}
state.Client.Token = token
state.Site = resp
if !state.Site.MyUser.IsValid() {
return
}
for _, c := range state.Site.MyUser.MustValue().Follows {
state.Session.Communities = append(state.Session.Communities, types.CommunityView{
Community: c.Community,
Subscribed: "Subscribed",
})
}
sort.Slice(state.Session.Communities, func(a, b int) bool {
return state.Session.Communities[a].Community.Name < state.Session.Communities[b].Community.Name
})
}
func (state *State) GetComment(commentid int) {
@ -227,9 +292,9 @@ func (state *State) GetComment(commentid int) {
state.CommentID = commentid
cresp, err := state.Client.Comments(context.Background(), types.GetComments{
ParentID: types.NewOptional(state.CommentID),
Sort: types.NewOptional(types.CommentSortType(state.Sort)),
Sort: types.NewOptional(types.CommentSortType(state.CommentSort)),
Type: types.NewOptional(types.ListingType("All")),
Limit: types.NewOptional(int64(200)),
Limit: types.NewOptional(int64(50)),
})
if err != nil {
fmt.Println(err)
@ -251,6 +316,33 @@ func (state *State) GetComment(commentid int) {
state.Comments = append(state.Comments, comment)
}
}
if len(state.Comments) == 0 {
return
}
ctx, err := state.GetContext(state.Context, state.Comments[0])
if err != nil {
fmt.Println(err)
} else {
state.Comments = []Comment{ctx}
}
}
func (state *State) GetContext(depth int, comment Comment) (ctx Comment, err error) {
if depth < 1 || comment.ParentID() == 0 {
return comment, nil
}
cresp, err := state.Client.Comment(context.Background(), types.GetComment{
ID: comment.ParentID(),
})
if err != nil {
return
}
ctx, err = state.GetContext(depth-1, Comment{
P: cresp.CommentView,
State: state,
C: []Comment{comment},
ChildCount: comment.ChildCount + 1,
})
return
}
func (state *State) GetComments() {
if state.Sort != "Hot" && state.Sort != "Top" && state.Sort != "Old" && state.Sort != "New" {
@ -258,9 +350,9 @@ func (state *State) GetComments() {
}
cresp, err := state.Client.Comments(context.Background(), types.GetComments{
PostID: types.NewOptional(state.PostID),
Sort: types.NewOptional(types.CommentSortType(state.Sort)),
Sort: types.NewOptional(types.CommentSortType(state.CommentSort)),
Type: types.NewOptional(types.ListingType("All")),
Limit: types.NewOptional(int64(200)),
Limit: types.NewOptional(int64(50)),
Page: types.NewOptional(int64(state.Page)),
})
if err != nil {
@ -343,6 +435,7 @@ func (state *State) GetMessages() {
Post: m.Post,
Creator: m.Creator,
Community: m.Community,
Counts: m.Counts,
},
Op: unread,
State: state,
@ -365,9 +458,11 @@ func (state *State) GetUser(username string) {
Username: types.NewOptional(state.UserName),
Page: types.NewOptional(int64(state.Page)),
Limit: types.NewOptional(int64(limit)),
SavedOnly: types.NewOptional(state.Op == "Saved"),
})
if err != nil {
fmt.Println(err)
state.Error = err
state.Status = http.StatusInternalServerError
return
}
@ -425,13 +520,16 @@ func (state *State) MarkAllAsRead() {
}
func (state *State) GetPosts() {
resp, err := state.Client.Posts(context.Background(), types.GetPosts{
posts := types.GetPosts{
Sort: types.NewOptional(types.SortType(state.Sort)),
Type: types.NewOptional(types.ListingType(state.Listing)),
CommunityName: types.NewOptional(state.CommunityName),
Limit: types.NewOptional(int64(25)),
Page: types.NewOptional(int64(state.Page)),
})
}
if state.CommunityName != "" {
posts.CommunityName = types.NewOptional(state.CommunityName)
}
resp, err := state.Client.Posts(context.Background(), posts)
if err != nil {
fmt.Println(err)
state.Status = http.StatusInternalServerError
@ -449,7 +547,18 @@ func (state *State) GetPosts() {
func (state *State) Search(searchtype string) {
if state.Query == "" && searchtype == "Communities" {
if state.Listing == "Subscribed" {
if state.Page > 1 {
return
}
if state.Site == nil {
state.GetSite()
}
state.Communities = state.Session.Communities
return
}
resp, err := state.Client.Communities(context.Background(), types.ListCommunities{
Type: types.NewOptional(types.ListingType(state.Listing)),
Sort: types.NewOptional(types.SortType(state.Sort)),
Limit: types.NewOptional(int64(25)),
Page: types.NewOptional(int64(state.Page)),
@ -464,7 +573,7 @@ func (state *State) Search(searchtype string) {
search := types.Search{
Q: state.Query,
Sort: types.NewOptional(types.SortType(state.Sort)),
ListingType: types.NewOptional(types.ListingType("All")),
ListingType: types.NewOptional(types.ListingType(state.Listing)),
Type: types.NewOptional(types.SearchType(searchtype)),
Limit: types.NewOptional(int64(25)),
Page: types.NewOptional(int64(state.Page)),
@ -492,9 +601,13 @@ func (state *State) Search(searchtype string) {
})
}
for _, c := range resp.Comments {
state.Comments = append(state.Comments, Comment{
comment := Comment{
P: c,
State: state,
}
state.Activities = append(state.Activities, Activity{
Timestamp: c.Comment.Published.Time,
Comment: &comment,
})
}
state.Communities = resp.Communities
@ -512,8 +625,9 @@ func (state *State) GetPost(postid int) {
})
if err != nil {
state.Status = http.StatusInternalServerError
state.Error = err
return
} else {
}
state.Posts = []Post{Post{
PostView: resp.PostView,
State: state,
@ -527,7 +641,6 @@ func (state *State) GetPost(postid int) {
Moderators: resp.Moderators,
}
state.Community = &cresp
}
}
func (state *State) GetCommunity(communityName string) {
@ -580,19 +693,19 @@ func (state *State) UploadImage(file multipart.File, header *multipart.FileHeade
func getChildren(parent *Comment, pool []types.CommentView, postCreatorID int) {
var children []Comment
total := -1
total := int32(0)
for _, c := range pool {
levels := strings.Split(c.Comment.Path, ".")
for i, l := range levels {
id, _ := strconv.Atoi(l)
if id == parent.P.Comment.ID {
total = total + 1
if i == (len(levels) - 2) {
children = append(children, Comment{
P: c,
C: children,
State: parent.State,
})
total += c.Counts.ChildCount
}
}
@ -600,7 +713,8 @@ func getChildren(parent *Comment, pool []types.CommentView, postCreatorID int) {
}
for i, _ := range children {
getChildren(&children[i], pool, postCreatorID)
parent.ChildCount += 1
}
parent.C = children
parent.ChildCount = total
parent.P.Counts.ChildCount -= total
}

View file

@ -3,16 +3,23 @@
<div class="activity">
{{ if $activity.Comment }}
<div class="title{{ if eq $activity.Comment.Op "unread"}} orangered{{end}}">
{{ if not $state.User }}
{{ if and (not $state.User) (not $state.Query) }}
<b>comment</b> on
{{ end }}
<a href="../post/{{ $activity.Comment.P.Post.ID}}">{{ $activity.Comment.P.Post.Name}}</a>
<a href="/{{$state.Host}}/post/{{ $activity.Comment.P.Post.ID}}">{{ $activity.Comment.P.Post.Name}}</a>
<span class="meta">
{{ if $state.User}}
by
<a href="">{{$state.User.PersonView.Person.Name }}</a>
{{ end }}
in
<a href="/{{$state.Host}}/c/{{ fullcname $activity.Comment.P.Community }}">/c/{{ $activity.Comment.P.Community.Name }}</a>
<a href="/{{$state.Host}}/c/{{ fullcname $activity.Comment.P.Community }}">
c/{{ if $state.HideInstanceNames -}}
{{ $activity.Comment.P.Community.Name }}</a>
{{ else -}}
{{ fullcname $activity.Comment.P.Community }}
{{ end }}
</span>
</div>
{{ template "comment.html" $activity.Comment }}
{{ else if $activity.Post }}
@ -23,10 +30,22 @@
<b>message</b>
{{ if eq $activity.Message.Creator.ID $state.Session.UserID }}
to
<a href="/{{$state.Host}}/u/{{fullname $activity.Message.Recipient}}">{{ $activity.Message.Recipient.Name }}</a>
<a href="/{{$state.Host}}/u/{{fullname $activity.Message.Recipient}}">
{{- if $state.HideInstanceNames -}}
{{ $activity.Message.Recipient.Name }}
{{- else -}}
{{ fullname $activity.Message.Recipient }}
{{- end -}}
</a>
{{ else }}
from
<a href="/{{$state.Host}}/u/{{fullname $activity.Message.Creator}}">{{ $activity.Message.Creator.Name }}</a>
<a href="/{{$state.Host}}/u/{{fullname $activity.Message.Creator}}">
{{- if $state.HideInstanceNames -}}
{{ $activity.Message.Creator.Name }}
{{- else -}}
{{ fullname $activity.Message.Creator }}
{{- end -}}
</a>
{{end}}
sent {{ humanize $activity.Message.PrivateMessage.Published.Time }}
</span>

View file

@ -1,57 +1,65 @@
<div class="comment{{if or (lt .P.Counts.Score -5) .P.Comment.Deleted }} hidden{{end}}" id="c{{.P.Comment.ID}}" onclick="commentClick(event)">
<div class="meta">
<div class="comment{{if or (lt .P.Counts.Score -5) .P.Comment.Deleted .P.Comment.Removed }} hidden{{end}}" id="c{{.P.Comment.ID}}">
{{ if .State.Session }}
<div class="score">
<form class="link-btn{{ if eq .P.MyVote.String "1"}} like{{ else if eq .P.MyVote.String "-1"}} dislike{{end}}" method="POST">
<input type="submit" name="vote" value="▲">
<input type="submit" name="submit" value="▲">
<div></div>
{{ if .P.MyVote.IsValid}}
<input type="hidden" name="undo" value="{{.P.MyVote.String}}">
{{ end}}
<input type="hidden" name="op" value="vote_comment">
<input type="hidden" name="commentid" value="{{.P.Comment.ID }}">
<input type="submit" name="vote" value="▼">
<input type="submit" name="submit" value="▼">
</form>
</div>
{{ end }}
<div class="meta">
<a class="minimize" href="" for="c{{.P.Comment.ID}}">
{{if or (lt .P.Counts.Score -5) .P.Comment.Deleted }}
{{- if or (lt .P.Counts.Score -5) .P.Comment.Deleted -}}
[+]
{{ else }}
{{- else -}}
[-]
{{ end }}
{{- end -}}
</a>
<a {{if .Submitter }}class="submitter"{{end}} href="/{{.State.Host}}/u/{{fullname .P.Creator}}">{{fullname .P.Creator}}</a>
{{.P.Counts.Score}} points <span title="{{.P.Comment.Published.Time}}">{{ humanize .P.Comment.Published.Time }}</span>
<a {{ if .P.Comment.Distinguished}}class="{{if .P.Creator.Admin}}admin {{end}}distinguished"{{ else if .Submitter }}class="submitter"{{end}} href="/{{.State.Host}}/u/{{fullname .P.Creator}}">
{{- if .State.HideInstanceNames -}}
{{ .P.Creator.Name }}
{{- else -}}
{{ fullname .P.Creator }}
{{- end -}}
</a>
<b>{{.P.Counts.Score}} points</b> <span title="{{.P.Comment.Published.Time}}">{{ humanize .P.Comment.Published.Time }}</span>
{{- if gt .P.Comment.Updated.Time.Unix .P.Comment.Published.Time.Unix -}}
* (last edited <span title="{{.P.Comment.Updated.Time}}">{{ humanize .P.Comment.Updated.Time }}</span>)
{{ end }}
</div>
<div class="content">
{{ if eq .Op "edit" }}
<form class="savecomment" method="POST">
<div>
<textarea required name="content">{{ .P.Comment.Content }}</textarea>
</div>
<input type="hidden" name="commentid" value="{{.P.Comment.ID}}">
<input type="hidden" name="op" value="edit_comment">
<input type="submit" value="save">
</form>
{{ template "create_comment.html" .State }}
{{ else }}
<div class="content{{ if and .Selected}} highlight{{end}}">{{if .P.Comment.Deleted}}[removed]{{else}}{{ markdown .State.Host .P.Comment.Content }}{{end}}</div>
{{if .P.Comment.Deleted}}
[deleted]
{{else if .P.Comment.Removed }}
[removed by mod]
{{else}}
<div {{ if and .Selected (not .State.XHR) (ne .State.Op "reply")}}class="highlight" {{end}}>
{{ markdown .State.Host .P.Comment.Content }}
</div>
{{end}}
{{ if eq .Op "source" }}
<div><textarea>{{.P.Comment.Content}}</textarea></div>
{{end}}
{{ end }}
<ul class="buttons">
<li><a href="/{{.State.Host}}/comment/{{.P.Comment.ID}}">permalink</a></li>
<li><a href="{{.P.Comment.ApID}}">fedilink</a></li>
{{ if ne .Op "source"}}
<li><a class="source" for="c{{.P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?source">source</a></li>
{{ else }}
<li><a class="source" for="c{{.P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?">hide source</a></li>
{{ end }}
{{ if .State.Session }}
{{ if and (eq .P.Comment.CreatorID .State.Session.UserID) (ne .Op "edit")}}
{{ if and (eq .P.Comment.CreatorID .State.Session.UserID) (ne .Op "edit") }}
<li><a class="edit" for="c{{.P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?edit">edit</a></li>
<li>
<form class="delete" method="POST">
@ -61,28 +69,49 @@
</form>
</li>
{{ end }}
{{ if ne .Op "reply"}}
<li><a class="reply" for="c{{.P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?reply">reply</a></li>
<li>
<form class="link-btn" method="POST">
<input type="hidden" name="commentid" value="{{.P.Comment.ID}}">
<input type="hidden" name="op" value="save_comment">
{{ if .P.Saved }}
<input type="submit" name="submit" value="unsave">
{{ else }}
<input type="submit" name="submit" value="save">
{{ end }}
</form>
</li>
<li>
<a class="reply" for="c{{.P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?reply">
reply
</a>
</li>
{{ end }}
{{ if and .ParentID .State.CommentID (not .State.XHR) }}
<li>
<a href="/{{.State.Host}}/comment/{{.ParentID}}">parent</a>
</li>
{{ end }}
{{ if and .ParentID (or .State.Activities .State.Query) }}
<li>
<a href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?context=3">context</a>
</li>
{{ end }}
{{ if and .State.PostID (gt (add .P.Counts.ChildCount .ChildCount) 0) }}
<li><a class="hidechildren" for="c{{.P.Comment.ID}}" href=""><span class="hide">hide</span><span class="show">show {{add .P.Counts.ChildCount .ChildCount }}</span> child comments</a></li>
{{ end }}
</ul>
</div>
<div class="children">
{{ if and (eq .State.Op "reply") (eq .State.CommentID .P.Comment.ID)}}
<form class="savecomment" method="POST">
<div>
<textarea required name="content"></textarea>
</div>
<input type="hidden" name="parentid" value="{{.P.Comment.ID}}">
<input type="hidden" name="op" value="create_comment">
<input type="submit" value="save">
</form>
{{ template "create_comment.html" .State }}
{{ end}}
{{ range $ci, $child := .C }}{{ template "comment.html" $child }}{{end}}
</div>
{{ if ne .P.Counts.ChildCount .ChildCount}}
{{ if and (ne .P.Counts.ChildCount .ChildCount) (not .State.Activities) (not .State.Query) }}
<div class="morecomments">
<a class="loadmore" for="c{{ .P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?">load more comments</a>
<span class="gray">({{ sub .P.Counts.ChildCount .ChildCount}} replies)</span>
</div>
{{end}}
</div>
</div>

View file

@ -1,17 +1,17 @@
<div class="community">
<form method="POST" class="member {{ membership .Subscribed }}">
<input name="op" type="submit" value="{{ membership .Subscribed}}">
<input type="hidden" name="communityid" value ="{{.Community.ID}}">
<input type="hidden" name="communityid" value ="{{ .Community.ID }}">
</form>
<span class="title"><a href="{{proxy .Community.ActorID}}">c/{{fullcname .Community}}: {{.Community.Title}}</a></span>
<span class="title"><a href="{{ if .Community.Local }}./c/{{.Community.Name}}{{else}}{{ localize .Community.ActorID }}{{end}}">c/{{fullcname .Community}}: {{.Community.Title}}</a></span>
<div class="details">
{{ if .Community.Description.IsValid }}
<div class="description">
{{markdown "poop" .Community.Description.String}}
{{ markdown "" .Community.Description.String }}
</div>
{{ end }}
<div class="gray">
{{printer .Counts.Subscribers}} subscribers,
{{ if .Counts.Subscribers }}{{ printer .Counts.Subscribers }} subscribers,{{end}}
a community founded {{ humanize .Community.Published.Time }}
</div>
</div>

View file

@ -0,0 +1,45 @@
<form class="savecomment" method="POST" enctype="multipart/form-data"
{{- if .CommentID }} action="/{{.Host}}/comment/{{.CommentID}}"
{{- else }} action="/{{.Host}}/post/{{.PostID}}"
{{- end -}}
>
<div class="upload">
<label title="upload photo"><div>📷</div>
<input class="imgupload" type="file" name="file" accept="image/*">
</label>
</div>
<div>
<textarea name="content">
{{- if .Content }}
{{- .Content -}}
{{ else if and (eq .Op "edit") .Comments }}
{{- (index .Comments 0).P.Comment.Content -}}
{{ end -}}
</textarea>
</div>
{{ if eq .Op "edit" }}
<input type="hidden" name="op" value="edit_comment">
<input type="hidden" name="commentid" value="{{.CommentID}}">
{{ else }}
<input type="hidden" name="op" value="create_comment">
{{ end }}
<input type="hidden" name="parentid" value="{{.CommentID}}">
<input type="submit" name="submit" value="save">
{{ if or .Op .Content }}
<input type="submit" name="submit" value="cancel">
{{ end }}
<div class="right">
<a href="https://join-lemmy.org/docs/users/02-media.html" target="_blank">formatting help</a>
<input name="submit" type="submit" value="preview">
</div>
{{ if .Content }}
<div class="preview">
<div class="comment">
<h3>Preview</h3>
<div class="content">
{{ markdown .Host .Content }}
</div>
</div>
</div>
{{ end }}
</form>

View file

@ -2,48 +2,49 @@
<head>
<title>{{ if and .Community (ne .Community.CommunityView.Community.Title "")}}{{.Community.CommunityView.Community.Title}}{{else if ne .CommunityName ""}}/c/{{.CommunityName}}{{ else if .User}}overview for {{.User.PersonView.Person.Name}}{{else}}{{ host .Host }}{{end}}</title>
<link rel="shortcut icon" href="/{{.Host}}/icon.jpg">
<link rel="stylesheet" href="/_/static/style.css?3">
<link rel="stylesheet" href="/_/static/style.css?v={{ .Version }}">
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body {{ if .Dark }}class="dark"{{end}}>
<noscript>
<style>
.expando-button {
display: none;
}
.comment .meta a.minimize {
display: none;
}
</style>
<link rel="stylesheet" href="/_/static/noscript.css?v={{ .Version }}">
</noscript>
{{ template "nav.html" . -}}
{{ if and (not .ShowNSFW) .Community .Community.CommunityView.Community.NSFW }}
{{ template "nsfw.html" }}
{{ else }}
<main>
{{ if or (contains .Sort "Top") (and (not .PostID) (not .User) (not .Community) (not .Activities) (eq .Op ""))}}
{{ template "menu.html" . }}
{{ end}}
{{ if .Error }}
<div class="error">{{.Error}}</div>
{{ end }}
{{ if .Error }}
<div class="error">
{{.Error}}.
{{ if .Unknown }}
try remote instance: <a href="{{ .Unknown }}">{{ .Unknown }}</a>
{{ end }}
</div>
{{ end }}
{{ range .Posts }}
{{ range .Posts }}
{{ template "post.html" . }}
{{ end }}
{{ end }}
{{ if or (and (not .Op) (not .Activities) (not .Comments) (not .Posts) (not .Communities)) (and (not .Comments) .PostID) (and (not .Activities) (not .Query) .User) }}
<div class="error">there doesn't seem to be anything here</div>
{{ end }}
{{ if or (and (not .Op) (not .Activities) (not .Comments) (not .Posts) (not .Communities)) (and (not .Comments) .PostID) (and (not .Activities) (not .Query) .User) }}
<div class="error">there doesn't seem to be anything here</div>
{{ end }}
{{ if or .Query (eq .SearchType "Communities") (eq (len .Posts) 25) (and .Comments (and (eq .CommentCount 200) (gt (index .Posts 0).Counts.Comments .CommentCount))) (and .User (or (gt .User.PersonView.Counts.CommentCount 10) (gt .User.PersonView.Counts.PostCount 10))) }}
{{ if or .Query (eq .SearchType "Communities") (eq (len .Posts) 25) (and .Comments (and (eq .CommentCount 200) (gt (index .Posts 0).Counts.Comments .CommentCount))) (and .User (or (gt .User.PersonView.Counts.CommentCount 10) (gt .User.PersonView.Counts.PostCount 10))) }}
<div class="pager">
view more: {{if gt .Page 1 }}<a href="{{ .PrevPage }}"> prev</a>{{ end }} <a href="{{ .NextPage }}">next </a>
</div>
{{ end }}
<input id="loadmore" type="submit" value="load more" data-page="2">
{{ end }}
{{ template "sidebar.html" . }}
</main>
<script src="/_/static/utils.js"></script>
{{ end }}
<script src="/_/static/utils.js?v={{ .Version }}"></script>
</body>
</html>

View file

@ -2,7 +2,7 @@
<head>
<title>{{ host .Host }}: sign up or log in</title>
<link rel="shortcut icon" href="/{{.Host}}/icon.jpg">
<link rel="stylesheet" href="/_/static/style.css?1">
<link rel="stylesheet" href="/_/static/style.css?v={{ .Version }}">
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body {{ if .Dark }}class="dark"{{end}}>
@ -38,7 +38,7 @@
{{ if ne .Op "2fa" }}
<div>
<h2>create a new account</h2>
<form method="POST">
<form method="POST" action="/{{ .Host}}/login">
<label>
username
<div><input required name="username" type="text"></div>
@ -85,7 +85,7 @@
{{ end }}
<div>
<h2>login</h2>
<form method="POST" action="/{{host .Host}}/login">
<form method="POST" action="/{{ .Host}}/login">
<label>
username
<div><input required name="username" type="text"></div>

View file

@ -3,28 +3,26 @@
<title>{{if and .Posts .PostID }}{{ (index .Posts 0).Post.Name}} : {{.CommunityName}}{{else if and .Community (ne .Community.CommunityView.Community.Title "")}}{{.Community.CommunityView.Community.Title}}{{else if ne .CommunityName ""}}/c/{{.CommunityName}}{{ else if .User}}overview for {{.User.PersonView.Person.Name}}{{else}}{{ host .Host }}{{end}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="/{{.Host}}/icon.jpg">
<link rel="stylesheet" href="/_/static/style.css?3">
<link rel="stylesheet" href="/_/static/style.css?v={{ .Version }}">
</head>
<body{{ if .Dark }} class="dark"{{end}}>
<noscript>
<style>
.expando-button {
display: none;
}
.comment .meta a.minimize {
display: none;
}
</style>
<link rel="stylesheet" href="/_/static/noscript.css?v={{ .Version }}">
</noscript>
{{ template "nav.html" . -}}
{{ if and (not .ShowNSFW) .Community .Community.CommunityView.Community.NSFW }}
{{ template "nsfw.html" }}
{{ else }}
<main>
{{ if or (contains .Sort "Top") (and (not .PostID) (not .User) (not .Community) (not .Activities) (eq .Op ""))}}
{{ if or (.Query) (.SearchType) (and (not .PostID) (not .User) (not .Activities) (eq .Op ""))}}
{{ template "menu.html" . }}
{{ end}}
{{ end}}
{{ if or (ne .Query "") .Communities }}
{{ if or (ne .Query "") .Communities }}
<form class="search" method="GET">
<input type="hidden" name="sort" value="{{.Sort}}">
<input type="hidden" name="listingType" value="{{.Listing}}">
<div>search</div>
<div class="query">
<input type="text" name="q" value="{{.Query}}">
@ -45,80 +43,93 @@
<input type="hidden" name="searchtype" value="Communities">
{{ end }}
</form>
{{ end}}
{{ end}}
{{ if .Error }}
<div class="error">{{.Error}}</div>
{{ end }}
{{ if .Error }}
<div class="error">
{{.Error}}.
{{ if .Unknown }}
try remote instance: <a href="{{ .Unknown }}">{{ .Unknown }}</a>
{{ end }}
</div>
{{ end }}
{{ range .Communities }}
{{ range .Communities }}
{{ template "community.html" . }}
{{ end }}
{{ end }}
{{ if eq .Op "create_community" "edit_community" }}
{{ if eq .Op "create_community" "edit_community" }}
{{ template "create_community.html" . }}
{{ end }}
{{ end }}
{{ range .Posts }}
{{ range .Posts }}
{{ template "post.html" . }}
{{ end }}
{{ end }}
{{ if eq .Op "create_post" "edit_post" }}
{{ if eq .Op "create_post" "edit_post" }}
{{ template "create_post.html" . }}
{{ end }}
{{ end }}
{{ if and .PostID .Posts}}
{{ if and .PostID .Posts}}
{{ if .CommentID}}
<div class="warning">you are viewing a single comment's thread<br>
<a href="../post/{{.PostID}}/#c{{.CommentID}}">view the rest of the comments</a>
</div>
{{ else }}
<div class="commentmenu">
{{if .Comments}}{{if gt .Page 1}}next{{else if or (lt .CommentCount 200) (lt (index .Posts 0).Counts.Comments .CommentCount) }}all{{else}}top{{end}} {{.CommentCount}} comments{{else}} no comments (yet){{end}}
{{if .Comments}}{{if gt .Page 1}}(page {{ .Page }}) {{else if lt (index .Posts 0).Counts.Comments .CommentCount }}all{{else}}top{{end}} {{.CommentCount}} comments{{else}} no comments (yet){{end}}
<div>
sorted by:
<a {{ if eq .Sort "Hot"}}class="selected"{{end}} href="{{ .SortBy "Hot"}}">hot</a>
<a {{ if eq .Sort "Top"}}class="selected"{{end}} href="{{ .SortBy "Top"}}">top</a>
<a {{ if eq .Sort "New"}}class="selected"{{end}} href="{{ .SortBy "New"}}">new</a>
<a {{ if eq .Sort "Old"}}class="selected"{{end}} href="{{ .SortBy "Old"}}">old</a>
<a {{ if eq .CommentSort "Hot"}}class="selected"{{end}} href="{{ .SortBy "Hot"}}">hot</a>
<a {{ if eq .CommentSort "Top"}}class="selected"{{end}} href="{{ .SortBy "Top"}}">top</a>
<a {{ if eq .CommentSort "New"}}class="selected"{{end}} href="{{ .SortBy "New"}}">new</a>
<a {{ if eq .CommentSort "Old"}}class="selected"{{end}} href="{{ .SortBy "Old"}}">old</a>
</div>
</div>
{{ if and .Session (ne .Op "edit_post") }}
<form class="savecomment" method="POST">
<div>
<textarea required name="content" {{ if (index .Posts 0).Post.Deleted }} disabled {{end}}></textarea>
<div class="create_comment">
{{ template "create_comment.html" .}}
</div>
<input type="hidden" name="op" value="create_comment">
<input type="submit" value="save"{{ if (index .Posts 0).Post.Deleted }} disabled {{end}}>
</form>
{{ end }}
{{ end }}
{{ end}}
{{ end}}
{{ range $i, $comment := .Comments }}
{{ range $i, $comment := .Comments }}
{{ template "comment.html" $comment }}
{{ end }}
{{ end }}
{{ if eq .Op "send_message" }}
{{ template "send_message.html" . }}
{{ else }}
{{ template "activities.html" . }}
{{ end }}
{{ if and .Comments .Posts (gt (index .Posts 0).Counts.Comments .CommentCount) (not .CommentID)}}
<div class="morecomments">
<a id="lmc" href="" data-page="2">load more comments</a>
</div>
{{ end }}
{{ if or (and (not .Op) (not .Activities) (not .Comments) (not .Posts) (not .Communities)) (and (not .Comments) .PostID) (and (not .Activities) (not .Query) .User) }}
<div class="error">there doesn't seem to be anything here</div>
{{ end }}
{{ if eq .Op "send_message" }}
{{ template "send_message.html" . }}
{{ else }}
{{ template "activities.html" . }}
{{ end }}
{{ if or (and (not .Op) (not .Activities) (not .Comments) (not .Posts) (not .Communities)) (and (not .Comments) .PostID) (and (not .Activities) (not .Query) .User) }}
<div class="error">there doesn't seem to be anything here</div>
{{ end }}
{{ if or .Query (eq .SearchType "Communities") (eq (len .Posts) 25) (and .Comments (and (eq .CommentCount 200) (gt (index .Posts 0).Counts.Comments .CommentCount))) (and .User (or (gt .User.PersonView.Counts.CommentCount 10) (gt .User.PersonView.Counts.PostCount 10))) }}
{{ if or .Query (eq .SearchType "Communities") (eq (len .Posts) 25) (and .Comments (gt (index .Posts 0).Counts.Comments .CommentCount) (not .CommentID)) (and .User (or (gt .User.PersonView.Counts.CommentCount 10) (gt .User.PersonView.Counts.PostCount 10))) }}
<div class="pager">
view more: {{if gt .Page 1 }}<a href="{{ .PrevPage }}"> prev</a>{{ end }} <a href="{{ .NextPage }}">next </a>
</div>
{{ end }}
{{ if not .PostID }}
<input id="loadmore" type="submit" value="load more" data-page="2">
{{ end }}
{{ end }}
<script src="/_/static/utils.js"></script>
<script src="/_/static/utils.js?v={{ .Version }}"></script>
{{ if .Watch }}
<script src="/_/static/ws.js"></script>
{{ end }}
{{ template "sidebar.html" . }}
</main>
{{ end }}
</body>
</html>

View file

@ -9,12 +9,18 @@
{{ end }}
{{ if contains .Sort "Top" }}
links from past:
<a {{ if eq .Sort "TopHour"}}class="selected"{{end}} href="{{ .SortBy "TopHour"}}">1h</a>
<span>-</span>
<a {{ if eq .Sort "TopSixHour"}}class="selected"{{end}} href="{{ .SortBy "TopSixHour"}}">6h</a>
<span>-</span>
<a {{ if eq .Sort "TopTwelveHour"}}class="selected"{{end}} href="{{ .SortBy "TopTwelveHour"}}">12h</a>
<span>-</span>
<a {{ if eq .Sort "TopDay"}}class="selected"{{end}} href="{{ .SortBy "TopDay"}}">day</a>
<span>-</span>
<a {{ if eq .Sort "TopMonth"}}class="selected"{{end}} href="{{ .SortBy "TopMonth"}}">month</a>
<span>-</span>
<a {{ if eq .Sort "TopWeek"}}class="selected"{{end}} href="{{ .SortBy "TopWeek"}}">week</a>
<span>-</span>
<a {{ if eq .Sort "TopMonth"}}class="selected"{{end}} href="{{ .SortBy "TopMonth"}}">month</a>
<span>-</span>
<a {{ if eq .Sort "TopYear"}}class="selected"{{end}} href="{{ .SortBy "TopYear"}}">year</a>
<span>-</span>
<a {{ if eq .Sort "TopAll"}}class="selected"{{end}} href="{{ .SortBy "TopAll"}}">all time</a>

View file

@ -1,15 +1,27 @@
{{ $state := . }}
<nav>
<div class="communities">
{{ if .Session }}
<a id="openmycommunities" href="/{{.Host}}/search?searchtype=Communities&listingType=Subscribed&sort=TopMonth&page=0">my communities ▼</a>
{{ end }}
<a href="/{{.Host}}">home</a>
<span> - </span>
<a href="/{{.Host}}?listingType=All">all</a>
|
{{ $host := .Host }}
{{ range $i, $c := .TopCommunities}}
<a href="/{{$host}}/c/{{$c.Community.Name}}">{{$c.Community.Name}}</a>
<a href="/{{$host}}/c/{{fullcname $c.Community}}">{{$c.Community.Name}}</a>
<span> - </span>
{{ end }}
<a href="/{{$host}}/search?searchtype=Communities" class="more">more »</a>
<a href="/{{$host}}/search?searchtype=Communities&sort=TopMonth" class="more">more »</a>
</div>
<div id="mycommunities">
{{- if and .Session .Session.Communities }}
<a href="/{{.Host}}/search?searchtype=Communities&listingType=Subscribed&sort=TopMonth&page=0">view all »</a>
{{ range .Session.Communities }}
<a href="/{{ $state.Host}}/{{ if .Community.Local }}c/{{.Community.Name}}{{else}}{{ localize .Community.ActorID }}{{end}}">{{fullcname .Community }}</a>
{{ end }}
{{ end -}}
</div>
<div class="right">
{{ if .Session }}
@ -17,15 +29,16 @@
|
<a href="/{{.Host}}/inbox" class="mailbox{{ if .UnreadCount }} orangered{{end}}"></a>
|
<a href="/{{.Host}}/settings">settings</a>
<a id="opensettings" href="/{{.Host}}/settings">settings</a>
|
<form method="POST"><input type="submit" name="op" value="logout"></form>
{{else}}
Want to join? <a href="/{{.Host}}/login">Log in</a> or <a href="/{{.Host}}/login">sign up</a> in seconds
<a href="/{{.Host}}/login">log in</a> or <a href="/{{.Host}}/login">sign up</a>
|
<a href="/{{.Host}}/settings">settings</a>
<a id="opensettings" href="/{{.Host}}/settings">settings</a>
{{end}}
</div>
<div id="settingspopup"></div>
<div class="spacer">
<a href="/{{ .Host}}/">
<img class="icon" src="{{ if .Site }}{{ .Site.SiteView.Site.Icon.String }}{{else}}/{{ .Host}}/icon.jpg{{end}}">
@ -47,11 +60,14 @@
<span>: search</span>
{{ end }}
<ul>
{{ if .User }}
<li class="selected"><a href="">overview</a></li>
{{ if and .User (not .Query)}}
<li {{if eq .Op "" }}class="selected"{{end}}><a href="?">overview</a></li>
{{ if and .Session (eq .User.PersonView.Person.ID .Session.UserID) }}
<li {{if eq .Op "Saved"}}class="selected"{{end}}><a href="?view=Saved">saved</a></li>
{{ end }}
{{ else if .Comments -}}
<li class="selected"><a href="">comments</a></li>
{{ else if .Activities }}
{{ else if and .Activities (not .Query) }}
<li class="selected"><a href="">mailbox</a></li>
{{ else }}
<li{{ if eq .Sort "Hot" }} class="selected"{{end}}><a href="{{ .SortBy "Hot" }}">hot</a></li>
@ -61,6 +77,9 @@
<li{{ if eq .Sort "MostComments" }} class="selected"{{end}}><a href="{{ .SortBy "MostComments" }}">most comments</a></li>
<li{{ if eq .Sort "NewComments" }} class="selected"{{end}}><a href="{{ .SortBy "NewComments" }}">new comments</a></li>
<li{{ if contains .Sort "Top" }} class="selected"{{end}}><a href="{{ .SortBy "TopDay" }}">top</a></li>
{{ if .Posts }}
<li id="showimages"><a id="se" href="">show images</a></li>
{{ end }}
{{ end }}
</ul>
{{ end }}

9
templates/nsfw.html Normal file
View file

@ -0,0 +1,9 @@
<form method="POST" class="nsfw">
<div>18+</div>
<h3>You must be 18+ to view this community</h3>
<p>You must be at least eighteen years old to view this content. Are you over eighteen and willing to see adult content?</p>
<input type="submit" name="submit" value="no thank you"> <input type="submit" name="submit" value="continue">
<input type="hidden" name="op" value="shownsfw">
</form>

View file

@ -1,12 +1,13 @@
{{ if not .State.XHR }}
<div class="post {{if .Post.Deleted}}deleted{{end}}" onclick="postClick(event)">
{{ if and (ne .State.Op "vote_post") (ne .State.Op "save_post") }}
<div class="post{{if .Post.Deleted}} deleted{{end}}{{ if .Post.FeaturedCommunity }} distinguished{{end}}{{if .Post.FeaturedLocal }} announcement{{end}}" id="p{{.Post.ID}}">
{{ if gt .Rank 0 }}
<div class="rank"> {{ .Rank }} </div>
{{ end }}
<div class="score">
{{ end }}
{{ if or (ne .State.Op "save_post") (eq .State.Op "vote_post") }}
{{ if .State.Session }}
<form class="link-btn {{ if lt .Rank 1 }}squish{{end}}{{ if eq .MyVote.String "1" }} like{{else if eq .MyVote.String "-1"}} dislike{{end}}" method="POST" onsubmit="formSubmit(event)">
<form class="link-btn {{ if lt .Rank 1 }}squish{{end}}{{ if eq .MyVote.String "1" }} like{{else if eq .MyVote.String "-1"}} dislike{{end}}" method="POST">
<input type="submit" name="vote" value="▲">
{{ if .MyVote.IsValid}}
<input type="hidden" name="undo" value="{{.MyVote.String}}">
@ -17,18 +18,24 @@
<input type="submit" name="vote" value="▼">
</form>
{{ else }}
<div style="margin-top: 19px;">{{ .Counts.Score }}</div>
<div style="margin-top: 19px;">{{ .Counts.Score }}</div>
{{ end }}
{{ if not .State.XHR}}
{{ end }}
{{ if and (ne .State.Op "vote_post") (ne .State.Op "save_post") }}
</div>
<div class="thumb" style="background-image: url({{if .Post.ThumbnailURL.IsValid}}{{.Post.ThumbnailURL.String}}?format=jpg&thumbnail=96{{else if .Post.URL.IsValid}}/_/static/link.png{{else}}/_/static/text.png{{end}})"></div>
{{ if not .State.HideThumbnails }}
<div class="thumb">
<a class="url" href="{{ if .Post.URL.IsValid }}{{ .Post.URL }}{{ else }}/{{ .State.Host }}/post/{{ .Post.ID }}{{ end }}">
<div {{ if and .Post.NSFW (not (and .State.Community .State.Community.CommunityView.Community.NSFW))}}class="img-blur"{{end}} style="background-image: url({{thumbnail .Post}})"></div>
</a>
</div>
{{ end }}
<div class="entry">
<div class="title">
<a class="url" href="{{ if .Post.URL.IsValid }}{{ .Post.URL }}{{ else }}/{{ .State.Host }}/post/{{ .Post.ID }}{{ end }}">{{ .Post.Name }}</a>
<a class="url" href="{{ if .Post.URL.IsValid }}{{ .Post.URL }}{{ else }}/{{ .State.Host }}/post/{{ .Post.ID }}{{ end }}">{{ rmmarkdown .Post.Name }}</a>
({{ domain . }})
</div>
<div class="expando-button {{ if and (not (and .Post.Body.IsValid .Post.Body.String )) (not (isImage .Post.URL.String)) }}hidden{{else if eq .Rank 0}}open{{ end }}"></div>
<div class="expando-button{{ if and (not (and .Post.Body.IsValid .Post.Body.String )) (not (isImage .Post.URL.String)) }} hidden{{else if eq .Rank 0}} open{{ end }}"></div>
<div class="meta">
submitted
<span title="{{.Post.Published.Time}}">{{ humanize .Post.Published.Time -}}</span>
@ -36,12 +43,26 @@
* (last edited <span title="{{.Post.Updated.Time}}">{{ humanize .Post.Updated.Time }}</span>)
{{ end }}
by
<a href="/{{ .State.Host }}/u/{{ fullname .Creator }}">{{ fullname .Creator }}</a>
<a class="submitter{{ if .Creator.Admin}} admin{{end}}" href="/{{ .State.Host }}/u/{{ fullname .Creator }}">
{{- if .State.HideInstanceNames -}}
{{ .Creator.Name }}
{{- else -}}
{{ fullname .Creator }}
{{- end -}}
</a>
to
<a href="/{{ .State.Host }}/c/{{ fullcname .Community }}">c/{{ fullcname .Community}}</a>
<a href="/{{ .State.Host }}/c/{{ fullcname .Community }}">
c/{{ if .State.HideInstanceNames -}}
{{ .Community.Name }}
{{ else -}}
{{ fullcname .Community }}
{{ end }}
</a>
</div>
<div class="buttons">
{{ if .Post.NSFW }}<span class="nsfw">NSFW</span>{{end}}
<a href="/{{ .State.Host }}/post/{{ .Post.ID }}">{{ .Counts.Comments }} comments</a>
<a href="{{ .Post.ApID}}">fedilink</a>
{{ if and .State.Session (eq .State.Session.UserID .Post.CreatorID) }}
{{ if not .Post.Deleted }}<a href="/{{ .State.Host }}/post/{{ .Post.ID }}?edit">edit</a>{{end}}
<form class="link-btn" method="POST">
@ -55,18 +76,49 @@
{{ end }}
</form>
{{ end}}
</div>
<div class="expando {{ if eq .Rank 0 }}open{{ end}}">
{{ if (and .Post.Body.IsValid (ne .Post.Body.String "")) }}
<div class="md">{{ markdown .State.Host .Post.Body.String }}</div>
{{ end }}
{{ if isImage .Post.URL.String}}
<img loading="lazy" src="{{ .Post.URL }}">
{{ if or (ne .State.Op "vote_post") (eq .State.Op "save_post") }}
{{ if .State.Session }}
<form class="link-btn" method="POST">
<input type="hidden" name="postid" value="{{.Post.ID }}">
<input type="hidden" name="op" value="save_post">
{{ if .PostView.Saved }}
<input type="submit" name="submit" value="unsave">
{{ else }}
<input type="submit" name="submit" value="save">
{{ end }}
</form>
{{end}}
{{ end }}
<div class="embed"></div>
{{ if and (ne .State.Op "vote_post") (ne .State.Op "save_post") }}
{{ if .State.PostID }}
<a id="hidechildren" class="scripting" href="">hide all child comments</a>
{{ end }}
{{ if and .State.Site .State.Site.MyUser.IsValid (not .State.Site.MyUser.MustValue.LocalUserView.LocalUser.ShowReadPosts) }}
<form class="link-btn" method="POST">
<input type="hidden" name="postid" value="{{.Post.ID }}">
<input type="hidden" name="op" value="read_post">
{{ if .Post.Deleted }}
<input type="submit" name="submit" value="mark unread">
{{ else }}
<input type="submit" name="submit" value="mark read">
{{ end }}
</form>
{{ end }}
</div>
</div>
<div></div>
<div class="clearleft"></div>
<div class="expando{{ if eq .Rank 0 }} open{{ end}}">
{{ if (and .Post.Body.IsValid (ne .Post.Body.String "")) }}
<div class="md">{{ markdown .State.Host .Post.Body.String }}</div>
{{ end }}
{{ if isImage .Post.URL.String}}
<div class="image" style="background-image: url({{.Post.URL}})">
<img loading="lazy" src="{{ .Post.URL }}">
</div>
{{ end }}
<div class="embed"></div>
</div>
</div>
{{ end }}

View file

@ -2,13 +2,13 @@
<head>
<title>mlmym</title>
<link rel="shortcut icon" href="/{{.Host}}/icon.jpg">
<link rel="stylesheet" href="/_/static/style.css">
<link rel="stylesheet" href="/_/static/style.css?v=7">
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<nav>
<div class="spacer"></div>
<span class="title">mlmym</span>
<span class="title">{{ .Title }}</span>
</nav>
<form class="root" method="POST">
<label>Enter a <a href="https://join-lemmy.org/instances" target="blank_">lemmy</a> domain or url

View file

@ -1,11 +1,15 @@
{{ if not .XHR }}
<!DOCTYPE html>
<head>
<title>{{ host .Host }}: preferences</title>
<link rel="shortcut icon" href="/{{.Host}}/icon.jpg">
<link rel="stylesheet" href="/_/static/style.css?1">
<link rel="stylesheet" href="/_/static/style.css?v={{ .Version }}">
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body {{ if .Dark}}class="dark"{{end}}>
<noscript>
<link rel="stylesheet" href="/_/static/noscript.css?v={{ .Version }}">
</noscript>
<nav>
<div class="communities">
<a href="/{{.Host}}">home</a>
@ -29,7 +33,7 @@
|
<form method="POST"><input type="submit" name="op" value="logout"></form>
{{else}}
Want to join? <a href="/{{.Host}}/login">Log in</a> or <a href="/{{.Host}}/login">sign up</a> in seconds
<a href="/{{.Host}}/login">log in</a> or <a href="/{{.Host}}/login">sign up</a>
|
<a href="/{{.Host}}/settings">settings</a>
{{end}}
@ -52,7 +56,8 @@
{{ if .Error }}
<div class="error">{{.Error}}</div>
{{ end }}
<form class="preferences" method="POST">
{{ end }}
<form id="settings" class="preferences" method="POST" target="/{{.Host}}/settings">
<div>
<label>
default listing
@ -65,7 +70,7 @@
</div>
<div>
<label>
default sort
default post sort
</label>
<select name="DefaultSortType">
<option value="Hot"{{ if eq .Sort "Hot"}} selected{{end}}>Hot</option>
@ -84,16 +89,55 @@
<option value="TopAll"{{ if eq .Sort "TopAll"}} selected{{end}}>Top All Time</option></select>
</select>
</div>
<div>
<label>
default comment sort
</label>
<select name="DefaultCommentSortType">
<option value="Hot"{{ if eq .CommentSort "Hot"}} selected{{end}}>Hot</option>
<option value="New"{{ if eq .CommentSort "New"}} selected{{end}}>New</option>
<option value="Old"{{ if eq .CommentSort "Old"}} selected{{end}}>Old</option>
<option value="Top"{{ if eq .CommentSort "Top"}} selected{{end}}>Top</option>
</select>
</div>
<div>
<label>
dark mode
</label>
<input type="checkbox" name="darkmode" {{ if .Dark }}checked{{end}}>
</div>
<div class="scripting">
<label>
endless scrolling
</label>
<input type="checkbox" name="endlessScrolling">
</div>
<div class="scripting">
<label>
auto load more
</label>
<input type="checkbox" name="autoLoad">
</div>
<div>
<label></label>
<label>
hide instance names
</label>
<input type="checkbox" name="hideInstanceNames" {{ if .HideInstanceNames }}checked{{end}}>
</div>
<div>
<label>
hide thumbnails
</label>
<input type="checkbox" name="hideThumbnails" {{ if .HideThumbnails }}checked{{end}}>
</div>
<div>
<label>lemmy: {{ .Site.Version }}<br><a href="https://github.com/rystaf/mlmym">mlmym</a>: {{ .Version }}</label>
<input type="submit" value="save">
{{ if .XHR }}<input id="closesettings" type="submit" value="close">{{ end }}
</div>
</form>
{{ if not .XHR}}
<script src="/_/static/utils.js?v={{ .Version }}"></script>
</body>
</html>
{{ end }}

View file

@ -1,16 +1,15 @@
{{ $host := .Host }}
<div class="{{ if .User }}user {{end}}side">
{{ if not .SearchType }}
<form method="GET" action="/{{.Host}}/search">
<input type="text" placeholder="search" name="q" value="{{.Query}}">
<input type="text" placeholder="search" name="q" value="">
{{ if .User }}
<input type="hidden" name="username" value="{{.UserName}}">
{{ else if .Community }}
<input type="hidden" name="communityname" value="{{.CommunityName}}">
<input type="hidden" name="communityname" value="{{fullcname .Community.CommunityView.Community}}">
{{ end }}
<input type="hidden" name="sort" value="New">
</form>
{{ end }}
{{ if .User }}
<h1>{{ .User.PersonView.Person.Name }}</h1>
@ -66,7 +65,7 @@
{{ printer .Site.SiteView.Counts.Users }} readers <br>
<span class="green" title="Users active in the last day"></span>
{{ printer .Site.SiteView.Counts.UsersActiveDay }} users here now
<p>{{ markdown .Host .Site.SiteView.Site.Sidebar.String }}</p>
{{ markdown .Host .Site.SiteView.Site.Sidebar.String }}
<div class="age" title="{{ .Site.SiteView.Site.Published.Time}}">founded {{ humanize .Site.SiteView.Site.Published.Time }}</div>
{{ if .Site.Admins }}
ADMINS
@ -91,8 +90,14 @@
<input name="op" type="submit" value="{{ membership .Community.CommunityView.Subscribed}}">
<input name="communityid" type="hidden" value="{{ .Community.CommunityView.Community.ID }}">
</form>
<form method="POST" class="block {{ if .Community.CommunityView.Blocked }}unblock{{end}}">
<input name="op" type="submit" value="{{ if .Community.CommunityView.Blocked}}unblock{{else}}block{{end}}">
<input name="communityid" type="hidden" value="{{ .Community.CommunityView.Community.ID }}">
</form>
{{ end }}
{{ .Community.CommunityView.Counts.Subscribers }} readers <br>
<div>
{{ .Community.CommunityView.Counts.Subscribers }} readers
</div>
<span class="green" title="Users active in the last day"></span>
{{ .Community.CommunityView.Counts.UsersActiveDay }} users here now
{{ if and .Session (isMod .Community .Session.UserName) }}

View file

@ -1,10 +1,21 @@
{{ if .CommentID }}
{{ $state := . }}
{{ if or .PostID .CommentID }}
{{ range $i, $comment := .Comments }}
{{ template "comment.html" $comment }}
{{ end }}
{{ else }}
{{ else if .Activities }}
{{ template "activities.html" . }}
{{ else if .Posts }}
{{ range $post := .Posts }}
{{ template "post.html" $post }}
{{ end }}
{{ else if .Communities }}
{{ range .Communities }}
{{ if not $state.Page }}
<a href="/{{ $state.Host}}/{{ if .Community.Local }}c/{{.Community.Name}}{{else}}{{ localize .Community.ActorID }}{{end}}">{{fullcname .Community }}</a>
{{ else }}
{{ template "community.html" . }}
{{ end }}
{{ end }}
{{ else }}
{{ end }}