Compare commits

...

34 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
26 changed files with 954 additions and 276 deletions

1
.gitignore vendored
View file

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

View file

@ -4,6 +4,7 @@ WORKDIR /app
COPY go.* ./ COPY go.* ./
RUN go mod download RUN go mod download
COPY . ./ COPY . ./
RUN git describe --tag > VERSION
RUN go build -v -o mlmym RUN go build -v -o mlmym
FROM debian:bullseye-slim 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/mlmym /app/mlmym
COPY --from=builder /app/templates /app/templates COPY --from=builder /app/templates /app/templates
COPY --from=builder /app/public /app/public COPY --from=builder /app/public /app/public
COPY --from=builder /app/VERSION /app/VERSION
CMD ["./mlmym", "--addr", "0.0.0.0:8080"] 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: all: mlmym
$(MAKE) -j3 --no-print-directory dev
dev: reload serve style mlmym: VERSION
go build -v -o mlmym
dev:
$(MAKE) -j2 --no-print-directory reload serve
reload: reload:
#websocketd --port=8080 watchexec -w public echo reload &>/dev/null websocketd --loglevel=fatal --port=8009 watchexec --no-vcs-ignore -e html,css,js 'echo "$$WATCHEXEC_WRITTEN_PATH"'
websocketd --loglevel=fatal --port=8009 watchexec --no-vcs-ignore -e html,css,js -d 500 -w public 'echo "$$WATCHEXEC_WRITTEN_PATH"'
serve: VERSION:
#python -m http.server --directory ./public 8081 &>/dev/null git describe --tag > $@
watchexec -e go -r "go run . --addr 0.0.0.0:8008 -w"
style: serve: VERSION
npm run watchcss > /dev/null 2>&1 DEBUG=true watchexec --no-vcs-ignore -e go -r "go run . --addr 0.0.0.0:8008 -w"

View file

@ -3,14 +3,23 @@ a familiar desktop experience for [lemmy](https://join-lemmy.org).
![screenshot](https://raw.githubusercontent.com/rystaf/mlmym/main/screenshot1.png?raw=true) ![screenshot](https://raw.githubusercontent.com/rystaf/mlmym/main/screenshot1.png?raw=true)
### deployment ## deployment
```bash ```bash
docker run -it -p "8080:8080" ghcr.io/rystaf/mlmym:latest 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 Set the environment variable `LEMMY_DOMAIN` to run in single instance mode
```bash ```bash
docker run -it -e LEMMY_DOMAIN='lemmydomain.com' -p "8080:8080" ghcr.io/rystaf/mlmym:latest 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 |

24
go.mod
View file

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

31
go.sum
View file

@ -2,45 +2,28 @@ 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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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-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 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 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/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/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 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY= github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY=
github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA= github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
github.com/rystaf/go-lemmy v0.0.0-20230622213726-c394de37235c h1:VxOcsDMWaqoBKbhoiSBxPl1zZ62YZ/VAW2nxlBRJiow= github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968 h1:wfyB6wQzYMH2U8xQvdamExbyCyPhe4o8HP47FMZM5Jk=
github.com/rystaf/go-lemmy v0.0.0-20230622213726-c394de37235c/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4=
github.com/rystaf/go-lemmy v0.0.0-20230622214853-5f2ab0756865 h1:xitFpcTOSP8RlZWR569yY75B2/7WX08rQQVG+0Mi4SA= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 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/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 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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/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/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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

45
main.go
View file

@ -7,12 +7,14 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"os"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
) )
var version string
var watch = flag.Bool("w", false, "watch for file changes") var watch = flag.Bool("w", false, "watch for file changes")
var addr = flag.String("addr", ":80", "http service address") var addr = flag.String("addr", ":80", "http service address")
var md goldmark.Markdown var md goldmark.Markdown
@ -54,7 +56,7 @@ func init() {
)) ))
templates = make(map[string]*template.Template) templates = make(map[string]*template.Template)
if !*watch { if !*watch {
for _, name := range []string{"index.html", "login.html", "frontpage.html", "root.html", "settings.html", "xhr.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) t := template.New(name).Funcs(funcMap)
glob, err := t.ParseGlob("templates/*") glob, err := t.ParseGlob("templates/*")
if err != nil { if err != nil {
@ -64,6 +66,47 @@ func init() {
templates[name] = glob 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 { func middleware(n httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 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

@ -219,6 +219,27 @@ summary {
font-size: 10px; font-size: 10px;
margin-bottom: 6px; margin-bottom: 6px;
} }
.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 { .meta a, .activity .meta a {
color: #369; color: #369;
text-decoration: none; text-decoration: none;
@ -262,6 +283,7 @@ summary {
} }
form.savecomment { form.savecomment {
margin: 0px 0px 10px 0px; margin: 0px 0px 10px 0px;
width: 500px;
} }
.comment > .children > form.savecomment { .comment > .children > form.savecomment {
margin: 0px 0px 10px 20px; margin: 0px 0px 10px 20px;
@ -270,9 +292,34 @@ form.savecomment {
margin: 5px 0px 10px 15px; margin: 5px 0px 10px 15px;
} }
.savecomment textarea { .savecomment textarea {
width: 500px; margin: 5px 0px;
width: 100%;
height: 100px; 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 { .comment .meta a.minimize {
color: #369; color: #369;
font-size: 10px; font-size: 10px;
@ -292,6 +339,7 @@ form.savecomment {
.children .morecomments { .children .morecomments {
} }
.morecomments { .morecomments {
height: 20px;
clear: left; clear: left;
margin: 0px 0px 10px 0px; margin: 0px 0px 10px 0px;
font-size: 10px; font-size: 10px;
@ -304,11 +352,11 @@ form.savecomment {
.morecomments a:hover { .morecomments a:hover {
text-decoration: underline; text-decoration: underline;
} }
.member { .member, .block {
display: inline-block; display: inline-block;
margin-bottom:3px; margin-bottom:3px;
} }
.member input { .member input, .block input {
font-size: 10px; font-size: 10px;
font-weight: bold; font-weight: bold;
display: inline-block; display: inline-block;
@ -320,10 +368,10 @@ form.savecomment {
bottom: 1px; bottom: 1px;
cursor: pointer; cursor: pointer;
} }
.join input { .join input, .block input {
background-color: green; background-color: green;
} }
.leave input { .leave input, .block.unblock input {
background-color: #cf6165; background-color: #cf6165;
} }
.pending input { .pending input {
@ -501,21 +549,52 @@ form.nsfw div {
position: relative; position: relative;
color: #000; color: #000;
} }
#settingspopup { #mycommunities, #settingspopup {
background-color: white; background-color: white;
border: 1px solid #888; border: 1px solid #888;
display: none; display: none;
position: absolute; 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; right: 10px;
top: 45px; top: 45px;
} }
#settingspopup form { #settingspopup form {
margin: 0px; margin: 0px;
} }
.dark #settingspopup { .dark #settingspopup, .dark #mycommunities {
background-color: #262626; background-color: #262626;
} }
#settingspopup.open { #settingspopup.open, #mycommunities.open {
display: inline-block; display: inline-block;
} }
.expando.open{ .expando.open{
@ -806,11 +885,11 @@ nav .communities a.more {
color: orangered !important; color: orangered !important;
} }
nav a { nav .communities a {
text-decoration: none; text-decoration: none;
color: black; color: black;
} }
nav > a:hover { nav .title a:hover {
text-decoration: underline; text-decoration: underline;
} }
@ -924,11 +1003,11 @@ nav .right a.mailbox {
top: 4px; top: 4px;
color: gray; color: gray;
} }
nav .right a, .right input[type=submit] { nav .right a, nav .right input[type=submit] {
color: #369; color: #369;
text-decoration: none; text-decoration: none;
} }
.dark nav .right a, .dark .right input[type=submit]{ .dark nav .right a, .dark nav .right input[type=submit]{
color: #dadada; color: #dadada;
} }
nav .right form, .comment form, form.link-btn { nav .right form, .comment form, form.link-btn {
@ -1025,9 +1104,14 @@ form.create input[type=file], form.create select {
font-size: 13px; font-size: 13px;
margin: 10px; margin: 10px;
} }
.preferences div:last-child label {
text-align: left;
font-size: 10px;
}
.preferences label{ .preferences label{
display: inline-block; display: inline-block;
width: 100px; width: 150px;
margin-right: 5px;
text-align: right; text-align: right;
} }

View file

@ -10,8 +10,6 @@ function request(url, params, callback, errorcallback = function(){}) {
var method = "GET" var method = "GET"
if (params) method = "POST" if (params) method = "POST"
xmlHttp.open(method, url, true); xmlHttp.open(method, url, true);
if (method = "POST")
xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xmlHttp.send(params); xmlHttp.send(params);
} }
function postClick(e) { function postClick(e) {
@ -34,25 +32,31 @@ function postClick(e) {
} }
} }
} }
function uptil (el, f) {
if (el) return f(el) ? el : uptil(el.parentNode, f)
}
function commentClick(e) { function commentClick(e) {
e = e || window.event; e = e || window.event;
var targ = e.currentTarget || e.srcElement || e; var targ = e.currentTarget || e.srcElement || e;
if (targ.nodeType == 3) targ = targ.parentNode; if (targ.nodeType == 3) targ = targ.parentNode;
if (e.target.name=="submit") { if (e.target.name=="submit") {
e.preventDefault() e.preventDefault()
var form = e.target.parentNode var form = uptil(e.target, function(el){ return el.tagName == "FORM" })
if (form) { if (form) {
data = new FormData(form) data = new FormData(form)
data.set(e.target.name, e.target.value)
data.set("xhr", 1)
if (("c"+data.get("commentid")) == targ.id) { 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) { } else if (("c"+data.get("parentid")) == targ.id) {
targ = form targ = form
} else { return } } else { return }
params = new URLSearchParams(data).toString()
params += "&" + e.target.name + "=" + e.target.value
params += "&xhr=1"
e.target.disabled = "disabled" e.target.disabled = "disabled"
request(targ.target || "", params, request(targ.action || "", data,
function(res){ function(res){
targ.outerHTML = res targ.outerHTML = res
setup() setup()
@ -145,7 +149,7 @@ function loadMoreComments(e) {
e.target.parentNode.outerHTML = res + '<div class="morecomments"><a id="lmc" href="" data-page="'+(parseInt(page)+1)+'">load more comments</a></div>' e.target.parentNode.outerHTML = res + '<div class="morecomments"><a id="lmc" href="" data-page="'+(parseInt(page)+1)+'">load more comments</a></div>'
setup() setup()
} else { } else {
e.target.parentNode.outerHTML = "" e.target.parentNode.innerHTML = ""
} }
}, function() { }, function() {
e.target.innerHTML = "loading failed" e.target.innerHTML = "loading failed"
@ -217,12 +221,15 @@ function formSubmit(e) {
var targ = e.currentTarget || e.srcElement || e; var targ = e.currentTarget || e.srcElement || e;
e.preventDefault() e.preventDefault()
var data = new FormData(targ) var data = new FormData(targ)
params = new URLSearchParams(data).toString() data.set(e.submitter.name, e.submitter.value)
params += "&" + e.submitter.name + "=" + e.submitter.value data.set("xhr", "1")
params += "&xhr=1"
e.submitter.disabled = "disabled" e.submitter.disabled = "disabled"
request(targ.target, params, request(targ.target, data,
function(res){ function(res){
if (data.get("op") == "read_post") {
document.getElementById("p"+data.get("postid")).remove()
return
}
targ.outerHTML = res targ.outerHTML = res
setup() setup()
}, },
@ -233,9 +240,33 @@ function formSubmit(e) {
return false 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) { function openSettings(e) {
e.preventDefault() e.preventDefault()
var settings = document.getElementById("settingspopup") var settings = document.getElementById("settingspopup")
if (settings.className == "open") {
settings.className = ""
return false
}
settings.className = "open" settings.className = "open"
request(e.target.href + "?xhr=1", "", function(res) { request(e.target.href + "?xhr=1", "", function(res) {
settings.innerHTML = res settings.innerHTML = res
@ -265,8 +296,7 @@ function saveSettings(e) {
var targ = e.currentTarget || e.srcElement || e; var targ = e.currentTarget || e.srcElement || e;
var data = new FormData(targ) var data = new FormData(targ)
e.preventDefault() e.preventDefault()
var params = new URLSearchParams(data).toString() request(targ.target, data, function(res) {
request(targ.target, params, function(res) {
["endlessScrolling", "autoLoad"].map(function(x) { ["endlessScrolling", "autoLoad"].map(function(x) {
localStorage.setItem(x, data.get(x)=="on") localStorage.setItem(x, data.get(x)=="on")
}) })
@ -321,6 +351,16 @@ function toggleImages(open) {
} }
} }
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() { function setup() {
if (showimages = document.getElementById("se")) { if (showimages = document.getElementById("se")) {
showimages.addEventListener("click", showImages) showimages.addEventListener("click", showImages)
@ -328,6 +368,9 @@ function setup() {
if (settings = document.getElementById("opensettings")) { if (settings = document.getElementById("opensettings")) {
settings.addEventListener("click", openSettings) settings.addEventListener("click", openSettings)
} }
if (settings = document.getElementById("openmycommunities")) {
settings.addEventListener("click", toggleMyCommunities)
}
if (hidechildren = document.getElementById("hidechildren")){ if (hidechildren = document.getElementById("hidechildren")){
hidechildren.addEventListener("click", hideAllChildComments) hidechildren.addEventListener("click", hideAllChildComments)
} }
@ -338,6 +381,10 @@ function setup() {
} }
lmc.addEventListener("click", loadMoreComments) 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") var posts = document.getElementsByClassName("post")
for (var i = 0; i < posts.length; i++) { for (var i = 0; i < posts.length; i++) {
posts[i].addEventListener("click", postClick) posts[i].addEventListener("click", postClick)

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()
}

339
routes.go
View file

@ -32,12 +32,12 @@ var funcMap = template.FuncMap{
} }
return host return host
}, },
"proxy": func(s string) string { "localize": func(s string) string {
u, err := url.Parse(s) u, err := url.Parse(s)
if err != nil { if err != nil {
return s return s
} }
return "/" + u.Host + u.Path return "." + u.Path + "@" + u.Host
}, },
"printer": func(n any) string { "printer": func(n any) string {
p := message.NewPrinter(language.English) p := message.NewPrinter(language.English)
@ -119,9 +119,9 @@ var funcMap = template.FuncMap{
if re.MatchString(p.URL.String()) { if re.MatchString(p.URL.String()) {
return p.URL.String() + "?format=jpg&thumbnail=96" return p.URL.String() + "?format=jpg&thumbnail=96"
} }
re = regexp.MustCompile(`^https:\/\/i.imgur.com\/([a-zA-Z0-9]+)\.([a-z]+)$`) re = regexp.MustCompile(`^https:\/\/(i\.)?imgur.com\/([a-zA-Z0-9]{5,})(\.[a-zA-Z0-9]+)?`)
if re.MatchString(p.URL.String()) { if re.MatchString(p.URL.String()) {
return re.ReplaceAllString(p.URL.String(), "https://i.imgur.com/${1}s.$2") return re.ReplaceAllString(p.URL.String(), "https://i.imgur.com/${2}s.jpg")
} }
if p.URL.IsValid() { if p.URL.IsValid() {
return "/_/static/link.png" return "/_/static/link.png"
@ -133,23 +133,17 @@ var funcMap = template.FuncMap{
var buf bytes.Buffer var buf bytes.Buffer
re := regexp.MustCompile(`\s---\s`) re := regexp.MustCompile(`\s---\s`)
body = re.ReplaceAllString(body, "\n***\n") 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 { if err := md.Convert([]byte(body), &buf); err != nil {
fmt.Println(err) fmt.Println(err)
return template.HTML(body) return template.HTML(body)
} }
converted := buf.String() body = buf.String()
converted = strings.Replace(converted, `<img `, `<img loading="lazy" `, -1) body = strings.Replace(body, `<img `, `<img loading="lazy" `, -1)
if os.Getenv("LEMMY_DOMAIN") == "" { body = LemmyLinkRewrite(body, host, os.Getenv("LEMMY_DOMAIN"))
re := regexp.MustCompile(`href="https:\/\/([a-zA-Z0-9\.\-]+\/(c\/[a-zA-Z0-9]+|(post|comment)\/\d+))`) body = RegReplace(body, `::: ?spoiler (.*?)\n([\S\s]*?):::`, "<details><summary>$1</summary>$2</details>")
converted = re.ReplaceAllString(converted, `href="/$1`) return template.HTML(body)
}
re = regexp.MustCompile(`href="\/(c\/[a-zA-Z0-9\-]+|(post|comment)\/\d+)`)
converted = re.ReplaceAllString(converted, `href="/`+host+`/$1`)
re = regexp.MustCompile(` !([a-zA-Z0-9]+)@([a-zA-Z0-9\.\-]+) `)
converted = re.ReplaceAllString(converted, ` <a href="/$2/c/$1">!$1@$2</a> `)
re = regexp.MustCompile(`::: spoiler (.*?)\n([\S\s]*?):::`)
converted = re.ReplaceAllString(converted, "<details><summary>$1</summary>$2</details>")
return template.HTML(converted)
}, },
"rmmarkdown": func(body string) string { "rmmarkdown": func(body string) string {
var buf bytes.Buffer var buf bytes.Buffer
@ -158,7 +152,7 @@ var funcMap = template.FuncMap{
return body return body
} }
text := html2text.HTML2TextWithOptions(buf.String(), html2text.WithLinksInnerText()) text := html2text.HTML2TextWithOptions(buf.String(), html2text.WithLinksInnerText())
re := regexp.MustCompile(`\<https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)\>`) 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, "") return re.ReplaceAllString(text, "")
}, },
"contains": strings.Contains, "contains": strings.Contains,
@ -170,11 +164,67 @@ var funcMap = template.FuncMap{
}, },
} }
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) { func Initialize(Host string, r *http.Request) (State, error) {
state := State{ state := State{
Host: Host, Host: Host,
Page: 1, Page: 1,
Status: http.StatusOK, Status: http.StatusOK,
Version: version,
}
if watch != nil {
state.Watch = *watch
} }
lemmyDomain := os.Getenv("LEMMY_DOMAIN") lemmyDomain := os.Getenv("LEMMY_DOMAIN")
if lemmyDomain != "" { if lemmyDomain != "" {
@ -209,14 +259,28 @@ func Initialize(Host string, r *http.Request) (State, error) {
} }
state.Listing = getCookie(r, "DefaultListingType") state.Listing = getCookie(r, "DefaultListingType")
state.Sort = getCookie(r, "DefaultSortType") 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.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) state.ParseQuery(r.URL.RawQuery)
if state.Sort == "" { 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" { if state.Listing == "" || state.Session == nil && state.Listing == "Subscribed" {
state.Listing = "All" state.Listing = getenv("LISTING", "All")
} }
return state, nil return state, nil
} }
@ -385,17 +449,84 @@ func GetFrontpage(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
} }
} }
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) { func GetPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
state, err := Initialize(ps.ByName("host"), r) state, err := Initialize(ps.ByName("host"), r)
if err != nil { if err != nil {
Render(w, "index.html", state) Render(w, "index.html", state)
return 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) m, _ := url.ParseQuery(r.URL.RawQuery)
if len(m["edit"]) > 0 { if len(m["edit"]) > 0 {
state.Op = "edit_post" state.Op = "edit_post"
state.GetSite() state.GetSite()
} }
if len(m["content"]) > 0 {
state.Content = m["content"][0]
}
postid, _ := strconv.Atoi(ps.ByName("postid")) postid, _ := strconv.Atoi(ps.ByName("postid"))
state.GetPost(postid) state.GetPost(postid)
state.GetComments() state.GetComments()
@ -407,6 +538,32 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
Render(w, "index.html", state) Render(w, "index.html", state)
return 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) m, _ := url.ParseQuery(r.URL.RawQuery)
if len(m["reply"]) > 0 { if len(m["reply"]) > 0 {
state.Op = "reply" state.Op = "reply"
@ -414,6 +571,9 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if len(m["edit"]) > 0 { if len(m["edit"]) > 0 {
state.Op = "edit" state.Op = "edit"
} }
if r.Method == "POST" && len(m["content"]) > 0 {
state.Content = m["content"][0]
}
if len(m["source"]) > 0 { if len(m["source"]) > 0 {
state.Op = "source" state.Op = "source"
} }
@ -423,6 +583,10 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
} }
commentid, _ := strconv.Atoi(ps.ByName("commentid")) commentid, _ := strconv.Atoi(ps.ByName("commentid"))
state.GetComment(commentid) state.GetComment(commentid)
if state.XHR && len(m["content"]) > 0 {
Render(w, "create_comment.html", state)
return
}
state.GetPost(state.PostID) state.GetPost(state.PostID)
Render(w, "index.html", state) Render(w, "index.html", state)
} }
@ -540,9 +704,10 @@ func Settings(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
Render(w, "index.html", state) Render(w, "index.html", state)
return return
} }
state.GetSite()
switch r.Method { switch r.Method {
case "POST": case "POST":
for _, name := range []string{"DefaultSortType", "DefaultListingType"} { for _, name := range []string{"DefaultSortType", "DefaultListingType", "DefaultCommentSortType"} {
deleteCookie(w, state.Host, name) deleteCookie(w, state.Host, name)
setCookie(w, "", name, r.FormValue(name)) setCookie(w, "", name, r.FormValue(name))
} }
@ -550,8 +715,7 @@ func Settings(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
setCookie(w, "", "Dark", "1") setCookie(w, "", "Dark", "1")
state.Dark = true state.Dark = true
} else { } else {
deleteCookie(w, state.Host, "Dark") setCookie(w, "", "Dark", "0")
deleteCookie(w, "", "Dark")
state.Dark = false state.Dark = false
} }
if r.FormValue("shownsfw") != "" { if r.FormValue("shownsfw") != "" {
@ -562,8 +726,23 @@ func Settings(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
deleteCookie(w, "", "ShowNSFW") deleteCookie(w, "", "ShowNSFW")
state.ShowNSFW = false 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.Listing = r.FormValue("DefaultListingType")
state.Sort = r.FormValue("DefaultSortType") state.Sort = r.FormValue("DefaultSortType")
state.CommentSort = r.FormValue("DefaultCommentSortType")
// TODO save user settings // TODO save user settings
case "GET": case "GET":
if state.Session != nil { if state.Session != nil {
@ -740,6 +919,18 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
CommunityID: communityid, CommunityID: communityid,
Follow: true, 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": case "logout":
deleteCookie(w, state.Host, "jwt") deleteCookie(w, state.Host, "jwt")
deleteCookie(w, state.Host, "user") deleteCookie(w, state.Host, "user")
@ -755,10 +946,15 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if err != nil { if err != nil {
if strings.Contains(fmt.Sprintf("%v", err), "missing_totp_token") { if strings.Contains(fmt.Sprintf("%v", err), "missing_totp_token") {
state.Op = "2fa" 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.Status = http.StatusUnauthorized
state.Error = err
Render(w, "login.html", state)
return
} else if resp.JWT.IsValid() { } else if resp.JWT.IsValid() {
state.GetUser(r.FormValue("username")) state.GetUser(r.FormValue("username"))
if state.User != nil { if state.User != nil {
@ -970,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.Path = "/" + state.Host + "/c/" + resp.PostView.Community.Name
r.URL.RawQuery = "" 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": case "vote_post":
var score int16 var score int16
score = 1 score = 1
@ -1023,9 +1235,20 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
parentid, _ := strconv.Atoi(r.FormValue("parentid")) parentid, _ := strconv.Atoi(r.FormValue("parentid"))
state.GetComment(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" { if r.FormValue("submit") == "save" {
createComment := types.CreateComment{ createComment := types.CreateComment{
Content: r.FormValue("content"), Content: content,
PostID: state.PostID, PostID: state.PostID,
} }
if state.CommentID > 0 { if state.CommentID > 0 {
@ -1047,6 +1270,22 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
} else { } else {
fmt.Println(err) 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") != "" { } else if r.FormValue("xhr") != "" {
w.Write([]byte{}) w.Write([]byte{})
return return
@ -1056,10 +1295,23 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
} }
case "edit_comment": case "edit_comment":
commentid, _ := strconv.Atoi(r.FormValue("commentid")) 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" { if r.FormValue("submit") == "save" {
resp, err := state.Client.EditComment(context.Background(), types.EditComment{ resp, err := state.Client.EditComment(context.Background(), types.EditComment{
CommentID: commentid, CommentID: commentid,
Content: types.NewOptional(r.FormValue("content")), Content: types.NewOptional(content),
}) })
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -1068,6 +1320,29 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
r.URL.Fragment = "c" + commentid r.URL.Fragment = "c" + commentid
r.URL.RawQuery = "" 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") != "" { if r.FormValue("xhr") != "" {
state.XHR = true state.XHR = true
@ -1136,6 +1411,7 @@ func GetRouter() *httprouter.Router {
router.POST("/:host/create_post", middleware(UserOp)) router.POST("/:host/create_post", middleware(UserOp))
router.GET("/:host/create_community", middleware(GetCreateCommunity)) router.GET("/:host/create_community", middleware(GetCreateCommunity))
router.POST("/:host/create_community", middleware(UserOp)) router.POST("/:host/create_community", middleware(UserOp))
router.GET("/:host/communities", middleware(GetCommunities))
} else { } else {
router.ServeFiles("/_/static/*filepath", http.Dir("public")) router.ServeFiles("/_/static/*filepath", http.Dir("public"))
router.GET("/", middleware(GetFrontpage)) router.GET("/", middleware(GetFrontpage))
@ -1167,6 +1443,7 @@ func GetRouter() *httprouter.Router {
router.POST("/create_post", middleware(UserOp)) router.POST("/create_post", middleware(UserOp))
router.GET("/create_community", middleware(GetCreateCommunity)) router.GET("/create_community", middleware(GetCreateCommunity))
router.POST("/create_community", middleware(UserOp)) router.POST("/create_community", middleware(UserOp))
router.GET("/communities", middleware(GetCommunities))
} }
return router return router
} }

180
state.go
View file

@ -11,6 +11,8 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"os"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -57,47 +59,85 @@ type Post struct {
} }
type Session struct { type Session struct {
UserName string UserName string
UserID int UserID int
Communities []types.CommunityView
} }
type State struct { type State struct {
Client *lemmy.Client Watch bool
HTTPClient *http.Client Version string
Session *Session Client *lemmy.Client
Status int HTTPClient *http.Client
Error error Session *Session
Alert string Status int
Host string Error error
CommunityName string Alert string
Community *types.GetCommunityResponse Host string
TopCommunities []types.CommunityView CommunityName string
Communities []types.CommunityView Community *types.GetCommunityResponse
UnreadCount int64 TopCommunities []types.CommunityView
Sort string Communities []types.CommunityView
Listing string UnreadCount int64
Page int Sort string
Parts []string CommentSort string
Posts []Post Listing string
Comments []Comment Page int
Activities []Activity Parts []string
CommentCount int Posts []Post
PostID int Comments []Comment
CommentID int Activities []Activity
Context int CommentCount int
UserName string PostID int
User *types.GetPersonDetailsResponse CommentID int
Now int64 Context int
XHR bool UserName string
Op string User *types.GetPersonDetailsResponse
Site *types.GetSiteResponse Now int64
Query string XHR bool
SearchType string Op string
Captcha *types.CaptchaResponse Site *types.GetSiteResponse
Dark bool Query string
ShowNSFW bool 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 { func (p State) SortBy(v string) string {
var q string var q string
if p.Query != "" || p.SearchType == "Communities" { if p.Query != "" || p.SearchType == "Communities" {
@ -163,6 +203,7 @@ func (state *State) ParseQuery(RawQuery string) {
} }
if len(m["sort"]) > 0 { if len(m["sort"]) > 0 {
state.Sort = m["sort"][0] state.Sort = m["sort"][0]
state.CommentSort = m["sort"][0]
} }
if len(m["communityname"]) > 0 { if len(m["communityname"]) > 0 {
state.CommunityName = m["communityname"][0] state.CommunityName = m["communityname"][0]
@ -221,17 +262,27 @@ func (state *State) GetCaptcha() {
} }
} }
func (state *State) GetSite() { func (state *State) GetSite() {
token := state.Client.Token
state.Client.Token = ""
resp, err := state.Client.Site(context.Background(), types.GetSite{}) resp, err := state.Client.Site(context.Background(), types.GetSite{})
if err != nil { if err != nil {
fmt.Println(err)
state.Status = http.StatusInternalServerError state.Status = http.StatusInternalServerError
state.Host = "." state.Host = "."
state.Error = errors.New("site unreachable") state.Error = errors.New("unable to retrieve site")
return return
} }
state.Client.Token = token
state.Site = resp 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) { func (state *State) GetComment(commentid int) {
@ -241,7 +292,7 @@ func (state *State) GetComment(commentid int) {
state.CommentID = commentid state.CommentID = commentid
cresp, err := state.Client.Comments(context.Background(), types.GetComments{ cresp, err := state.Client.Comments(context.Background(), types.GetComments{
ParentID: types.NewOptional(state.CommentID), 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")), Type: types.NewOptional(types.ListingType("All")),
Limit: types.NewOptional(int64(50)), Limit: types.NewOptional(int64(50)),
}) })
@ -265,6 +316,9 @@ func (state *State) GetComment(commentid int) {
state.Comments = append(state.Comments, comment) state.Comments = append(state.Comments, comment)
} }
} }
if len(state.Comments) == 0 {
return
}
ctx, err := state.GetContext(state.Context, state.Comments[0]) ctx, err := state.GetContext(state.Context, state.Comments[0])
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -296,7 +350,7 @@ func (state *State) GetComments() {
} }
cresp, err := state.Client.Comments(context.Background(), types.GetComments{ cresp, err := state.Client.Comments(context.Background(), types.GetComments{
PostID: types.NewOptional(state.PostID), 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")), Type: types.NewOptional(types.ListingType("All")),
Limit: types.NewOptional(int64(50)), Limit: types.NewOptional(int64(50)),
Page: types.NewOptional(int64(state.Page)), Page: types.NewOptional(int64(state.Page)),
@ -408,6 +462,7 @@ func (state *State) GetUser(username string) {
}) })
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
state.Error = err
state.Status = http.StatusInternalServerError state.Status = http.StatusInternalServerError
return return
} }
@ -492,7 +547,18 @@ func (state *State) GetPosts() {
func (state *State) Search(searchtype string) { func (state *State) Search(searchtype string) {
if state.Query == "" && searchtype == "Communities" { 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{ resp, err := state.Client.Communities(context.Background(), types.ListCommunities{
Type: types.NewOptional(types.ListingType(state.Listing)),
Sort: types.NewOptional(types.SortType(state.Sort)), Sort: types.NewOptional(types.SortType(state.Sort)),
Limit: types.NewOptional(int64(25)), Limit: types.NewOptional(int64(25)),
Page: types.NewOptional(int64(state.Page)), Page: types.NewOptional(int64(state.Page)),
@ -507,7 +573,7 @@ func (state *State) Search(searchtype string) {
search := types.Search{ search := types.Search{
Q: state.Query, Q: state.Query,
Sort: types.NewOptional(types.SortType(state.Sort)), 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)), Type: types.NewOptional(types.SearchType(searchtype)),
Limit: types.NewOptional(int64(25)), Limit: types.NewOptional(int64(25)),
Page: types.NewOptional(int64(state.Page)), Page: types.NewOptional(int64(state.Page)),
@ -559,22 +625,22 @@ func (state *State) GetPost(postid int) {
}) })
if err != nil { if err != nil {
state.Status = http.StatusInternalServerError state.Status = http.StatusInternalServerError
state.Error = err
return return
} else {
state.Posts = []Post{Post{
PostView: resp.PostView,
State: state,
}}
if state.CommentID > 0 && len(state.Posts) > 0 {
state.Posts[0].Rank = -1
}
state.CommunityName = resp.PostView.Community.Name
cresp := types.GetCommunityResponse{
CommunityView: resp.CommunityView,
Moderators: resp.Moderators,
}
state.Community = &cresp
} }
state.Posts = []Post{Post{
PostView: resp.PostView,
State: state,
}}
if state.CommentID > 0 && len(state.Posts) > 0 {
state.Posts[0].Rank = -1
}
state.CommunityName = resp.PostView.Community.Name
cresp := types.GetCommunityResponse{
CommunityView: resp.CommunityView,
Moderators: resp.Moderators,
}
state.Community = &cresp
} }
func (state *State) GetCommunity(communityName string) { func (state *State) GetCommunity(communityName string) {

View file

@ -13,7 +13,12 @@
<a href="">{{$state.User.PersonView.Person.Name }}</a> <a href="">{{$state.User.PersonView.Person.Name }}</a>
{{ end }} {{ end }}
in 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> </span>
</div> </div>
{{ template "comment.html" $activity.Comment }} {{ template "comment.html" $activity.Comment }}
@ -25,10 +30,22 @@
<b>message</b> <b>message</b>
{{ if eq $activity.Message.Creator.ID $state.Session.UserID }} {{ if eq $activity.Message.Creator.ID $state.Session.UserID }}
to 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 }} {{ else }}
from 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}} {{end}}
sent {{ humanize $activity.Message.PrivateMessage.Published.Time }} sent {{ humanize $activity.Message.PrivateMessage.Published.Time }}
</span> </span>

View file

@ -1,4 +1,4 @@
<div class="comment{{if or (lt .P.Counts.Score -5) .P.Comment.Deleted }} hidden{{end}}" id="c{{.P.Comment.ID}}"> <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 }} {{ if .State.Session }}
<div class="score"> <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"> <form class="link-btn{{ if eq .P.MyVote.String "1"}} like{{ else if eq .P.MyVote.String "-1"}} dislike{{end}}" method="POST">
@ -15,44 +15,44 @@
{{ end }} {{ end }}
<div class="meta"> <div class="meta">
<a class="minimize" href="" for="c{{.P.Comment.ID}}"> <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 .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> </a>
<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}}">{{fullname .P.Creator}}</a>
<b>{{.P.Counts.Score}} points</b> <span title="{{.P.Comment.Published.Time}}">{{ humanize .P.Comment.Published.Time }}</span> <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 -}} {{- 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>) * (last edited <span title="{{.P.Comment.Updated.Time}}">{{ humanize .P.Comment.Updated.Time }}</span>)
{{ end }} {{ end }}
</div> </div>
<div class="content">
{{ if eq .Op "edit" }} {{ if eq .Op "edit" }}
<div class="content"> {{ template "create_comment.html" .State }}
<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 name="submit" type="submit" value="save">
<input name="submit" type="submit" value="cancel">
</form>
{{ else }} {{ else }}
<div class="content"> {{if .P.Comment.Deleted}}
{{if .P.Comment.Deleted}} [deleted]
[removed] {{else if .P.Comment.Removed }}
{{else}} [removed by mod]
{{else}}
<div {{ if and .Selected (not .State.XHR) (ne .State.Op "reply")}}class="highlight" {{end}}> <div {{ if and .Selected (not .State.XHR) (ne .State.Op "reply")}}class="highlight" {{end}}>
{{ markdown .State.Host .P.Comment.Content }} {{ markdown .State.Host .P.Comment.Content }}
</div> </div>
{{end}} {{end}}
{{ if eq .Op "source" }} {{ if eq .Op "source" }}
<div><textarea>{{.P.Comment.Content}}</textarea></div> <div><textarea>{{.P.Comment.Content}}</textarea></div>
{{end}} {{end}}
{{ end }} {{ end }}
<ul class="buttons"> <ul class="buttons">
<li><a href="/{{.State.Host}}/comment/{{.P.Comment.ID}}">permalink</a></li> <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"}} {{ if ne .Op "source"}}
<li><a class="source" for="c{{.P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?source">source</a></li> <li><a class="source" for="c{{.P.Comment.ID}}" href="/{{.State.Host}}/comment/{{.P.Comment.ID}}?source">source</a></li>
{{ else }} {{ else }}
@ -87,7 +87,7 @@
</a> </a>
</li> </li>
{{ end }} {{ end }}
{{ if and .ParentID .State.CommentID }} {{ if and .ParentID .State.CommentID (not .State.XHR) }}
<li> <li>
<a href="/{{.State.Host}}/comment/{{.ParentID}}">parent</a> <a href="/{{.State.Host}}/comment/{{.ParentID}}">parent</a>
</li> </li>
@ -104,15 +104,7 @@
</div> </div>
<div class="children"> <div class="children">
{{ if and (eq .State.Op "reply") (eq .State.CommentID .P.Comment.ID)}} {{ if and (eq .State.Op "reply") (eq .State.CommentID .P.Comment.ID)}}
<form class="savecomment" method="POST"> {{ template "create_comment.html" .State }}
<div>
<textarea name="content"></textarea>
</div>
<input type="hidden" name="parentid" value="{{.P.Comment.ID}}">
<input type="hidden" name="op" value="create_comment">
<input type="submit" name="submit" value="save">
<input type="submit" name="submit" value="cancel">
</form>
{{ end}} {{ end}}
{{ range $ci, $child := .C }}{{ template "comment.html" $child }}{{end}} {{ range $ci, $child := .C }}{{ template "comment.html" $child }}{{end}}
{{ if and (ne .P.Counts.ChildCount .ChildCount) (not .State.Activities) (not .State.Query) }} {{ if and (ne .P.Counts.ChildCount .ChildCount) (not .State.Activities) (not .State.Query) }}
@ -122,4 +114,4 @@
</div> </div>
{{end}} {{end}}
</div> </div>
</div> </div>

View file

@ -1,17 +1,17 @@
<div class="community"> <div class="community">
<form method="POST" class="member {{ membership .Subscribed }}"> <form method="POST" class="member {{ membership .Subscribed }}">
<input name="op" type="submit" value="{{ 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> </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"> <div class="details">
{{ if .Community.Description.IsValid }} {{ if .Community.Description.IsValid }}
<div class="description"> <div class="description">
{{markdown "poop" .Community.Description.String}} {{ markdown "" .Community.Description.String }}
</div> </div>
{{ end }} {{ end }}
<div class="gray"> <div class="gray">
{{printer .Counts.Subscribers}} subscribers, {{ if .Counts.Subscribers }}{{ printer .Counts.Subscribers }} subscribers,{{end}}
a community founded {{ humanize .Community.Published.Time }} a community founded {{ humanize .Community.Published.Time }}
</div> </div>
</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,26 +2,12 @@
<head> <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> <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="shortcut icon" href="/{{.Host}}/icon.jpg">
<link rel="stylesheet" href="/_/static/style.css?v=23"> <link rel="stylesheet" href="/_/static/style.css?v={{ .Version }}">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
</head> </head>
<body {{ if .Dark }}class="dark"{{end}}> <body {{ if .Dark }}class="dark"{{end}}>
<noscript> <noscript>
<style> <link rel="stylesheet" href="/_/static/noscript.css?v={{ .Version }}">
.expando-button,
.minimize,
#loadmore,
#showimages,
.hidechildren {
display: none;
}
div.pager {
display: block;
}
.post .expando .image img {
visibility: visible;
}
</style>
</noscript> </noscript>
{{ template "nav.html" . -}} {{ template "nav.html" . -}}
@ -29,12 +15,15 @@
{{ template "nsfw.html" }} {{ template "nsfw.html" }}
{{ else }} {{ else }}
<main> <main>
{{ if or (contains .Sort "Top") (and (not .PostID) (not .User) (not .Community) (not .Activities) (eq .Op ""))}} {{ template "menu.html" . }}
{{ template "menu.html" . }}
{{ end}}
{{ if .Error }} {{ if .Error }}
<div class="error">{{.Error}}</div> <div class="error">
{{.Error}}.
{{ if .Unknown }}
try remote instance: <a href="{{ .Unknown }}">{{ .Unknown }}</a>
{{ end }}
</div>
{{ end }} {{ end }}
{{ range .Posts }} {{ range .Posts }}
@ -56,6 +45,6 @@
{{ template "sidebar.html" . }} {{ template "sidebar.html" . }}
</main> </main>
{{ end }} {{ end }}
<script src="/_/static/utils.js?v=19"></script> <script src="/_/static/utils.js?v={{ .Version }}"></script>
</body> </body>
</html> </html>

View file

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

View file

@ -3,23 +3,11 @@
<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> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="/{{.Host}}/icon.jpg"> <link rel="shortcut icon" href="/{{.Host}}/icon.jpg">
<link rel="stylesheet" href="/_/static/style.css?v=23"> <link rel="stylesheet" href="/_/static/style.css?v={{ .Version }}">
</head> </head>
<body{{ if .Dark }} class="dark"{{end}}> <body{{ if .Dark }} class="dark"{{end}}>
<noscript> <noscript>
<style> <link rel="stylesheet" href="/_/static/noscript.css?v={{ .Version }}">
.scripting,
.expando-button,
.minimize,
#showimages,
#lmc,
.hidechildren {
display: none !important;
}
.post .expando .image img {
visibility: visible;
}
</style>
</noscript> </noscript>
{{ template "nav.html" . -}} {{ template "nav.html" . -}}
@ -27,12 +15,14 @@
{{ template "nsfw.html" }} {{ template "nsfw.html" }}
{{ else }} {{ else }}
<main> <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" . }} {{ template "menu.html" . }}
{{ end}} {{ end}}
{{ if or (ne .Query "") .Communities }} {{ if or (ne .Query "") .Communities }}
<form class="search" method="GET"> <form class="search" method="GET">
<input type="hidden" name="sort" value="{{.Sort}}">
<input type="hidden" name="listingType" value="{{.Listing}}">
<div>search</div> <div>search</div>
<div class="query"> <div class="query">
<input type="text" name="q" value="{{.Query}}"> <input type="text" name="q" value="{{.Query}}">
@ -56,7 +46,12 @@
{{ end}} {{ end}}
{{ if .Error }} {{ if .Error }}
<div class="error">{{.Error}}</div> <div class="error">
{{.Error}}.
{{ if .Unknown }}
try remote instance: <a href="{{ .Unknown }}">{{ .Unknown }}</a>
{{ end }}
</div>
{{ end }} {{ end }}
{{ range .Communities }} {{ range .Communities }}
@ -85,20 +80,16 @@
{{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}} {{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> <div>
sorted by: sorted by:
<a {{ if eq .Sort "Hot"}}class="selected"{{end}} href="{{ .SortBy "Hot"}}">hot</a> <a {{ if eq .CommentSort "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 .CommentSort "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 .CommentSort "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 "Old"}}class="selected"{{end}} href="{{ .SortBy "Old"}}">old</a>
</div> </div>
</div> </div>
{{ if and .Session (ne .Op "edit_post") }} {{ if and .Session (ne .Op "edit_post") }}
<form class="savecomment" method="POST"> <div class="create_comment">
<div> {{ template "create_comment.html" .}}
<textarea required name="content" {{ if (index .Posts 0).Post.Deleted }} disabled {{end}}></textarea> </div>
</div>
<input type="hidden" name="op" value="create_comment">
<input type="submit" name="submit" value="save"{{ if (index .Posts 0).Post.Deleted }} disabled {{end}}>
</form>
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ end}} {{ end}}
@ -133,7 +124,10 @@
{{ end }} {{ end }}
{{ end }} {{ end }}
<script src="/_/static/utils.js?v=19"></script> <script src="/_/static/utils.js?v={{ .Version }}"></script>
{{ if .Watch }}
<script src="/_/static/ws.js"></script>
{{ end }}
{{ template "sidebar.html" . }} {{ template "sidebar.html" . }}
</main> </main>
{{ end }} {{ end }}

View file

@ -9,12 +9,18 @@
{{ end }} {{ end }}
{{ if contains .Sort "Top" }} {{ if contains .Sort "Top" }}
links from past: 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> <a {{ if eq .Sort "TopDay"}}class="selected"{{end}} href="{{ .SortBy "TopDay"}}">day</a>
<span>-</span> <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> <a {{ if eq .Sort "TopWeek"}}class="selected"{{end}} href="{{ .SortBy "TopWeek"}}">week</a>
<span>-</span> <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> <a {{ if eq .Sort "TopYear"}}class="selected"{{end}} href="{{ .SortBy "TopYear"}}">year</a>
<span>-</span> <span>-</span>
<a {{ if eq .Sort "TopAll"}}class="selected"{{end}} href="{{ .SortBy "TopAll"}}">all time</a> <a {{ if eq .Sort "TopAll"}}class="selected"{{end}} href="{{ .SortBy "TopAll"}}">all time</a>

View file

@ -1,5 +1,9 @@
{{ $state := . }}
<nav> <nav>
<div class="communities"> <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> <a href="/{{.Host}}">home</a>
<span> - </span> <span> - </span>
<a href="/{{.Host}}?listingType=All">all</a> <a href="/{{.Host}}?listingType=All">all</a>
@ -9,7 +13,15 @@
<a href="/{{$host}}/c/{{fullcname $c.Community}}">{{$c.Community.Name}}</a> <a href="/{{$host}}/c/{{fullcname $c.Community}}">{{$c.Community.Name}}</a>
<span> - </span> <span> - </span>
{{ end }} {{ 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>
<div class="right"> <div class="right">
{{ if .Session }} {{ if .Session }}
@ -48,14 +60,14 @@
<span>: search</span> <span>: search</span>
{{ end }} {{ end }}
<ul> <ul>
{{ if .User }} {{ if and .User (not .Query)}}
<li {{if eq .Op "" }}class="selected"{{end}}><a href="?">overview</a></li> <li {{if eq .Op "" }}class="selected"{{end}}><a href="?">overview</a></li>
{{ if and .Session (eq .User.PersonView.Person.ID .Session.UserID) }} {{ 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> <li {{if eq .Op "Saved"}}class="selected"{{end}}><a href="?view=Saved">saved</a></li>
{{ end }} {{ end }}
{{ else if .Comments -}} {{ else if .Comments -}}
<li class="selected"><a href="">comments</a></li> <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> <li class="selected"><a href="">mailbox</a></li>
{{ else }} {{ else }}
<li{{ if eq .Sort "Hot" }} class="selected"{{end}}><a href="{{ .SortBy "Hot" }}">hot</a></li> <li{{ if eq .Sort "Hot" }} class="selected"{{end}}><a href="{{ .SortBy "Hot" }}">hot</a></li>

View file

@ -1,5 +1,5 @@
{{ if and (ne .State.Op "vote_post") (ne .State.Op "save_post") }} {{ 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}}"> <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 }} {{ if gt .Rank 0 }}
<div class="rank"> {{ .Rank }} </div> <div class="rank"> {{ .Rank }} </div>
{{ end }} {{ end }}
@ -23,17 +23,19 @@
{{ end }} {{ end }}
{{ if and (ne .State.Op "vote_post") (ne .State.Op "save_post") }} {{ if and (ne .State.Op "vote_post") (ne .State.Op "save_post") }}
</div> </div>
{{ if not .State.HideThumbnails }}
<div class="thumb"> <div class="thumb">
<a class="url" href="{{ if .Post.URL.IsValid }}{{ .Post.URL }}{{ else }}/{{ .State.Host }}/post/{{ .Post.ID }}{{ end }}"> <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> <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> </a>
</div> </div>
{{ end }}
<div class="entry"> <div class="entry">
<div class="title"> <div class="title">
<a class="url" href="{{ if .Post.URL.IsValid }}{{ .Post.URL }}{{ else }}/{{ .State.Host }}/post/{{ .Post.ID }}{{ end }}">{{ rmmarkdown .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 . }}) ({{ domain . }})
</div> </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"> <div class="meta">
submitted submitted
<span title="{{.Post.Published.Time}}">{{ humanize .Post.Published.Time -}}</span> <span title="{{.Post.Published.Time}}">{{ humanize .Post.Published.Time -}}</span>
@ -41,13 +43,26 @@
* (last edited <span title="{{.Post.Updated.Time}}">{{ humanize .Post.Updated.Time }}</span>) * (last edited <span title="{{.Post.Updated.Time}}">{{ humanize .Post.Updated.Time }}</span>)
{{ end }} {{ end }}
by by
<a class="submitter{{ if .Creator.Admin}} admin{{end}}" 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 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>
<div class="buttons"> <div class="buttons">
{{ if .Post.NSFW }}<span class="nsfw">NSFW</span>{{end}} {{ if .Post.NSFW }}<span class="nsfw">NSFW</span>{{end}}
<a href="/{{ .State.Host }}/post/{{ .Post.ID }}">{{ .Counts.Comments }} comments</a> <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 and .State.Session (eq .State.Session.UserID .Post.CreatorID) }}
{{ if not .Post.Deleted }}<a href="/{{ .State.Host }}/post/{{ .Post.ID }}?edit">edit</a>{{end}} {{ if not .Post.Deleted }}<a href="/{{ .State.Host }}/post/{{ .Post.ID }}?edit">edit</a>{{end}}
<form class="link-btn" method="POST"> <form class="link-btn" method="POST">
@ -79,11 +94,22 @@
{{ if .State.PostID }} {{ if .State.PostID }}
<a id="hidechildren" class="scripting" href="">hide all child comments</a> <a id="hidechildren" class="scripting" href="">hide all child comments</a>
{{ end }} {{ 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></div> <div></div>
<div class="clearleft"></div> <div class="clearleft"></div>
<div class="expando {{ if eq .Rank 0 }}open{{ end}}"> <div class="expando{{ if eq .Rank 0 }} open{{ end}}">
{{ if (and .Post.Body.IsValid (ne .Post.Body.String "")) }} {{ if (and .Post.Body.IsValid (ne .Post.Body.String "")) }}
<div class="md">{{ markdown .State.Host .Post.Body.String }}</div> <div class="md">{{ markdown .State.Host .Post.Body.String }}</div>
{{ end }} {{ end }}

View file

@ -3,16 +3,12 @@
<head> <head>
<title>{{ host .Host }}: preferences</title> <title>{{ host .Host }}: preferences</title>
<link rel="shortcut icon" href="/{{.Host}}/icon.jpg"> <link rel="shortcut icon" href="/{{.Host}}/icon.jpg">
<link rel="stylesheet" href="/_/static/style.css?v=15"> <link rel="stylesheet" href="/_/static/style.css?v={{ .Version }}">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
</head> </head>
<body {{ if .Dark}}class="dark"{{end}}> <body {{ if .Dark}}class="dark"{{end}}>
<noscript> <noscript>
<style> <link rel="stylesheet" href="/_/static/noscript.css?v={{ .Version }}">
.scripting {
display: none;
}
</style>
</noscript> </noscript>
<nav> <nav>
<div class="communities"> <div class="communities">
@ -74,7 +70,7 @@
</div> </div>
<div> <div>
<label> <label>
default sort default post sort
</label> </label>
<select name="DefaultSortType"> <select name="DefaultSortType">
<option value="Hot"{{ if eq .Sort "Hot"}} selected{{end}}>Hot</option> <option value="Hot"{{ if eq .Sort "Hot"}} selected{{end}}>Hot</option>
@ -93,6 +89,17 @@
<option value="TopAll"{{ if eq .Sort "TopAll"}} selected{{end}}>Top All Time</option></select> <option value="TopAll"{{ if eq .Sort "TopAll"}} selected{{end}}>Top All Time</option></select>
</select> </select>
</div> </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> <div>
<label> <label>
dark mode dark mode
@ -112,13 +119,25 @@
<input type="checkbox" name="autoLoad"> <input type="checkbox" name="autoLoad">
</div> </div>
<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"> <input type="submit" value="save">
{{ if .XHR }}<input id="closesettings" type="submit" value="close">{{ end }} {{ if .XHR }}<input id="closesettings" type="submit" value="close">{{ end }}
</div> </div>
</form> </form>
{{ if not .XHR}} {{ if not .XHR}}
<script src="/_/static/utils.js?v=8"></script> <script src="/_/static/utils.js?v={{ .Version }}"></script>
</body> </body>
</html> </html>
{{ end }} {{ end }}

View file

@ -1,16 +1,15 @@
{{ $host := .Host }} {{ $host := .Host }}
<div class="{{ if .User }}user {{end}}side"> <div class="{{ if .User }}user {{end}}side">
{{ if not .SearchType }}
<form method="GET" action="/{{.Host}}/search"> <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 }} {{ if .User }}
<input type="hidden" name="username" value="{{.UserName}}"> <input type="hidden" name="username" value="{{.UserName}}">
{{ else if .Community }} {{ else if .Community }}
<input type="hidden" name="communityname" value="{{fullcname .Community.CommunityView.Community}}"> <input type="hidden" name="communityname" value="{{fullcname .Community.CommunityView.Community}}">
{{ end }} {{ end }}
<input type="hidden" name="sort" value="New">
</form> </form>
{{ end }}
{{ if .User }} {{ if .User }}
<h1>{{ .User.PersonView.Person.Name }}</h1> <h1>{{ .User.PersonView.Person.Name }}</h1>
@ -91,8 +90,14 @@
<input name="op" type="submit" value="{{ membership .Community.CommunityView.Subscribed}}"> <input name="op" type="submit" value="{{ membership .Community.CommunityView.Subscribed}}">
<input name="communityid" type="hidden" value="{{ .Community.CommunityView.Community.ID }}"> <input name="communityid" type="hidden" value="{{ .Community.CommunityView.Community.ID }}">
</form> </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 }} {{ 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> <span class="green" title="Users active in the last day"></span>
{{ .Community.CommunityView.Counts.UsersActiveDay }} users here now {{ .Community.CommunityView.Counts.UsersActiveDay }} users here now
{{ if and .Session (isMod .Community .Session.UserName) }} {{ if and .Session (isMod .Community .Session.UserName) }}

View file

@ -1,11 +1,21 @@
{{ $state := . }}
{{ if or .PostID .CommentID }} {{ if or .PostID .CommentID }}
{{ range $i, $comment := .Comments }} {{ range $i, $comment := .Comments }}
{{ template "comment.html" $comment }} {{ template "comment.html" $comment }}
{{ end }} {{ end }}
{{ else if .Activities }} {{ else if .Activities }}
{{ template "activities.html" . }} {{ template "activities.html" . }}
{{ else }} {{ else if .Posts }}
{{ range $post := .Posts }} {{ range $post := .Posts }}
{{ template "post.html" $post }} {{ template "post.html" $post }}
{{ end }} {{ 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 }} {{ end }}