diff --git a/.gitignore b/.gitignore index c9a2389..3d7be86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ mlmym +VERSION *.toml *.txt diff --git a/Dockerfile b/Dockerfile index 1bd6c58..9b9ab73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ WORKDIR /app COPY go.* ./ RUN go mod download COPY . ./ +RUN git describe --tag > VERSION RUN go build -v -o mlmym FROM debian:bullseye-slim @@ -14,4 +15,5 @@ RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install - COPY --from=builder /app/mlmym /app/mlmym COPY --from=builder /app/templates /app/templates COPY --from=builder /app/public /app/public +COPY --from=builder /app/VERSION /app/VERSION CMD ["./mlmym", "--addr", "0.0.0.0:8080"] diff --git a/Makefile b/Makefile index ca918f2..1b88d82 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,18 @@ -.PHONY: dev reload serve style +.PHONY: dev reload serve VERSION -all: - $(MAKE) -j3 --no-print-directory dev +all: mlmym -dev: reload serve style +mlmym: VERSION + go build -v -o mlmym + +dev: + $(MAKE) -j2 --no-print-directory reload serve reload: - #websocketd --port=8080 watchexec -w public echo reload &>/dev/null - websocketd --loglevel=fatal --port=8009 watchexec --no-vcs-ignore -e html,css,js -d 500 -w public 'echo "$$WATCHEXEC_WRITTEN_PATH"' + websocketd --loglevel=fatal --port=8009 watchexec --no-vcs-ignore -e html,css,js 'echo "$$WATCHEXEC_WRITTEN_PATH"' -serve: - #python -m http.server --directory ./public 8081 &>/dev/null - watchexec -e go -r "go run . --addr 0.0.0.0:8008 -w" +VERSION: + git describe --tag > $@ -style: - npm run watchcss > /dev/null 2>&1 +serve: VERSION + DEBUG=true watchexec --no-vcs-ignore -e go -r "go run . --addr 0.0.0.0:8008 -w" diff --git a/README.md b/README.md index ee99f9e..c2bde67 100644 --- a/README.md +++ b/README.md @@ -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) -### deployment +## deployment ```bash docker run -it -p "8080:8080" ghcr.io/rystaf/mlmym:latest ``` -### config +## config Set the environment variable `LEMMY_DOMAIN` to run in single instance mode ```bash docker run -it -e LEMMY_DOMAIN='lemmydomain.com' -p "8080:8080" ghcr.io/rystaf/mlmym:latest ``` +#### default user settings +| environment variable | default | +| -------------------- | ------- | +| DARK | false | +| HIDE_THUMBNAILS | false | +| LISTING | All | +| SORT | Hot | +| COMMENT_SORT | Hot | + diff --git a/go.mod b/go.mod index 1eb85a5..062f0de 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,16 @@ module mlmym go 1.19 require ( - github.com/cenkalti/backoff/v4 v4.2.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/gorilla/securecookie v1.1.1 // indirect - github.com/gorilla/sessions v1.2.1 // indirect - github.com/gorilla/websocket v1.4.2 // indirect - github.com/julienschmidt/httprouter v1.3.0 // indirect - github.com/k3a/html2text v1.2.1 // indirect - github.com/rystaf/go-lemmy v0.0.0-20230704005320-c4b010dd339b // indirect - github.com/yuin/goldmark v1.5.4 // indirect - go.elara.ws/go-lemmy v0.17.3 // indirect - golang.org/x/text v0.10.0 // indirect + github.com/dustin/go-humanize v1.0.1 + github.com/julienschmidt/httprouter v1.3.0 + github.com/k3a/html2text v1.2.1 + github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968 + github.com/yuin/goldmark v1.5.4 + golang.org/x/text v0.10.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.2.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect ) diff --git a/go.sum b/go.sum index 7dfa6d5..984409b 100644 --- a/go.sum +++ b/go.sum @@ -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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY= github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA= -github.com/rystaf/go-lemmy v0.0.0-20230622213726-c394de37235c h1:VxOcsDMWaqoBKbhoiSBxPl1zZ62YZ/VAW2nxlBRJiow= -github.com/rystaf/go-lemmy v0.0.0-20230622213726-c394de37235c/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= -github.com/rystaf/go-lemmy v0.0.0-20230622214853-5f2ab0756865 h1:xitFpcTOSP8RlZWR569yY75B2/7WX08rQQVG+0Mi4SA= -github.com/rystaf/go-lemmy v0.0.0-20230622214853-5f2ab0756865/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= -github.com/rystaf/go-lemmy v0.0.0-20230622215253-d38b61ec174f h1:EueAC5v+8oX9xK9bT36Tpgbz+c66wUZx5zmyxePurbw= -github.com/rystaf/go-lemmy v0.0.0-20230622215253-d38b61ec174f/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= -github.com/rystaf/go-lemmy v0.0.0-20230622222647-983d49e1d285 h1:tihBOF3ejTXzYVftaflwqRAXnaY4W9q3iNiE3YMF+D8= -github.com/rystaf/go-lemmy v0.0.0-20230622222647-983d49e1d285/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= -github.com/rystaf/go-lemmy v0.0.0-20230622230518-ee2cfdf288a4 h1:++T5SoZzghtfNJprWlXiRSpPPdnMSSZgIWWAnPoGx/w= -github.com/rystaf/go-lemmy v0.0.0-20230622230518-ee2cfdf288a4/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= -github.com/rystaf/go-lemmy v0.0.0-20230623185656-962f9bf8359d h1:ORS2KIBuT+wBn4wJncF1SoLDCVCAUPHASHpQ+Y3TnRI= -github.com/rystaf/go-lemmy v0.0.0-20230623185656-962f9bf8359d/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= -github.com/rystaf/go-lemmy v0.0.0-20230623191111-7ff8c74b1935 h1:zmzUz6PGRB8yQTT6BRaZNTgNlrk6L7e72dzTnWJTw+I= -github.com/rystaf/go-lemmy v0.0.0-20230623191111-7ff8c74b1935/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= -github.com/rystaf/go-lemmy v0.0.0-20230623191350-f39e3c8bdcb5 h1:MoI87uid2KqpLdUMZGK2HBOuxJMnPOJaar/4Og2PshM= -github.com/rystaf/go-lemmy v0.0.0-20230623191350-f39e3c8bdcb5/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= -github.com/rystaf/go-lemmy v0.0.0-20230704005320-c4b010dd339b h1:6z+gOUUvKwKQfgqEbxXS229gjr5V3HYg9bYbL9VHFdQ= -github.com/rystaf/go-lemmy v0.0.0-20230704005320-c4b010dd339b/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= +github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968 h1:wfyB6wQzYMH2U8xQvdamExbyCyPhe4o8HP47FMZM5Jk= +github.com/rystaf/go-lemmy v0.0.0-20230720221045-c6d79b98e968/go.mod h1:nRSkTD+ARAHXtqlSPdf5q3hjHLP1ALsS1m5D3o86o+4= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.elara.ws/go-lemmy v0.17.3 h1:644k23BS2xqKJHJ9cHd8eyt1INpb5myqsBQQL2chBiA= -go.elara.ws/go-lemmy v0.17.3/go.mod h1:rurQND/HT3yWfX/T4w+hb6vEwRAeAlV+9bSGFkkx5rA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/main.go b/main.go index ca577c2..95e0f3d 100644 --- a/main.go +++ b/main.go @@ -7,12 +7,14 @@ import ( "log" "net" "net/http" + "os" "github.com/julienschmidt/httprouter" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" ) +var version string var watch = flag.Bool("w", false, "watch for file changes") var addr = flag.String("addr", ":80", "http service address") var md goldmark.Markdown @@ -54,7 +56,7 @@ func init() { )) templates = make(map[string]*template.Template) 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) glob, err := t.ParseGlob("templates/*") if err != nil { @@ -64,6 +66,47 @@ func init() { templates[name] = glob } } + if os.Getenv("DEBUG") != "" { + test() + } + if data, err := os.ReadFile("VERSION"); err == nil { + version = string(data) + } +} +func test() { + links := [][]string{ + []string{"https://lemmy.local/u/dude", "/lemmy.local/u/dude", "/u/dude"}, + []string{"https://lemmy.local/u/dude@lemmy.local", "/lemmy.local/u/dude", "/u/dude"}, + []string{"/u/dude", "/lemmy.local/u/dude", "/u/dude"}, + []string{"/u/dude@lemmy.world", "/lemmy.local/u/dude@lemmy.world", "/u/dude@lemmy.world"}, + []string{"/u/dude@lemmy.local", "/lemmy.local/u/dude", "/u/dude"}, + []string{"https://lemmy.world/c/dude", "/lemmy.local/c/dude@lemmy.world", "/c/dude@lemmy.world"}, + []string{"https://lemmy.world/u/dude", "/lemmy.local/u/dude@lemmy.world", "/u/dude@lemmy.world"}, + []string{"https://lemmy.world/u/dude@lemmy.world", "/lemmy.local/u/dude@lemmy.world", "/u/dude@lemmy.world"}, + []string{"https://lemmy.world/post/123", "/lemmy.local/post/123@lemmy.world", "/post/123@lemmy.world"}, + []string{"https://lemmy.world/post/123#123", "https://lemmy.world/post/123#123", "https://lemmy.world/post/123#123"}, + []string{"/post/123", "/lemmy.local/post/123", "/post/123"}, + []string{"/comment/123", "/lemmy.local/comment/123", "/comment/123"}, + []string{"https://lemmy.local/comment/123", "/lemmy.local/comment/123", "/comment/123"}, + } + for _, url := range links { + output := LemmyLinkRewrite(`href="`+url[0]+`"`, "lemmy.local", "") + success := (output == (`href="` + url[1] + `"`)) + if !success { + fmt.Println("\n!!!! multi instance link rewrite failure !!!!") + fmt.Println(url) + fmt.Println(output) + fmt.Println("") + } + output = LemmyLinkRewrite(`href="`+url[0]+`"`, ".", "lemmy.local") + success = (output == (`href="` + url[2] + `"`)) + if !success { + fmt.Println("\n!!!! single instance link rewrite failure !!!!") + fmt.Println(success, url) + fmt.Println(output) + fmt.Println("") + } + } } func middleware(n httprouter.Handle) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { diff --git a/public/noscript.css b/public/noscript.css new file mode 100644 index 0000000..8b24e2a --- /dev/null +++ b/public/noscript.css @@ -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; +} diff --git a/public/style.css b/public/style.css index 36214aa..7522f61 100644 --- a/public/style.css +++ b/public/style.css @@ -219,6 +219,27 @@ summary { font-size: 10px; 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 { color: #369; text-decoration: none; @@ -262,6 +283,7 @@ summary { } form.savecomment { margin: 0px 0px 10px 0px; + width: 500px; } .comment > .children > form.savecomment { margin: 0px 0px 10px 20px; @@ -270,9 +292,34 @@ form.savecomment { margin: 5px 0px 10px 15px; } .savecomment textarea { - width: 500px; + margin: 5px 0px; + width: 100%; height: 100px; } + +.savecomment .upload label div { + display: inline-block; + border: 1px solid #ccc; + height: 20px; + line-height: 20px; + width: 32px; + position: relative; + background-color: #999; + color: #000; + text-align: center; + cursor: pointer; +} +.savecomment .upload input { + display: none; +} +.savecomment .right { + float:right; +} +.savecomment .right a { + line-height: 28px; + font-size: 10px; +} + .comment .meta a.minimize { color: #369; font-size: 10px; @@ -292,6 +339,7 @@ form.savecomment { .children .morecomments { } .morecomments { + height: 20px; clear: left; margin: 0px 0px 10px 0px; font-size: 10px; @@ -304,11 +352,11 @@ form.savecomment { .morecomments a:hover { text-decoration: underline; } -.member { +.member, .block { display: inline-block; margin-bottom:3px; } -.member input { +.member input, .block input { font-size: 10px; font-weight: bold; display: inline-block; @@ -320,10 +368,10 @@ form.savecomment { bottom: 1px; cursor: pointer; } -.join input { +.join input, .block input { background-color: green; } -.leave input { +.leave input, .block.unblock input { background-color: #cf6165; } .pending input { @@ -501,21 +549,52 @@ form.nsfw div { position: relative; color: #000; } -#settingspopup { +#mycommunities, #settingspopup { background-color: white; border: 1px solid #888; display: none; position: absolute; + z-index: 100; +} +#mycommunities { + top: 17px; + padding: 5px 0px; + border-width: 0px 1px 1px 0px; +} +#mycommunities div { + margin: 0px 5px; +} +#mycommunities a:first-child { + text-align: right; +} +#mycommunities a { + text-decoration: none; + color: #369; + text-transform: uppercase; + font-size: 9px; + display: block; + padding: 0px 3px; +} +.dark #mycommunities a { + color: #8cb3d9; +} +.dark #mycommunities a:hover { + background-color: #3e3e3e; +} +#mycommunities a:hover { + background-color: #c7def7; +} +#settingspopup { right: 10px; top: 45px; } #settingspopup form { margin: 0px; } -.dark #settingspopup { +.dark #settingspopup, .dark #mycommunities { background-color: #262626; } -#settingspopup.open { +#settingspopup.open, #mycommunities.open { display: inline-block; } .expando.open{ @@ -806,11 +885,11 @@ nav .communities a.more { color: orangered !important; } -nav a { +nav .communities a { text-decoration: none; color: black; } -nav > a:hover { +nav .title a:hover { text-decoration: underline; } @@ -924,11 +1003,11 @@ nav .right a.mailbox { top: 4px; color: gray; } -nav .right a, .right input[type=submit] { +nav .right a, nav .right input[type=submit] { color: #369; text-decoration: none; } -.dark nav .right a, .dark .right input[type=submit]{ +.dark nav .right a, .dark nav .right input[type=submit]{ color: #dadada; } nav .right form, .comment form, form.link-btn { @@ -1025,9 +1104,14 @@ form.create input[type=file], form.create select { font-size: 13px; margin: 10px; } +.preferences div:last-child label { + text-align: left; + font-size: 10px; +} .preferences label{ display: inline-block; - width: 100px; + width: 150px; + margin-right: 5px; text-align: right; } diff --git a/public/utils.js b/public/utils.js index ae67624..df8c22a 100644 --- a/public/utils.js +++ b/public/utils.js @@ -10,8 +10,6 @@ function request(url, params, callback, errorcallback = function(){}) { var method = "GET" if (params) method = "POST" xmlHttp.open(method, url, true); - if (method = "POST") - xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); xmlHttp.send(params); } function postClick(e) { @@ -34,25 +32,31 @@ function postClick(e) { } } } +function uptil (el, f) { + if (el) return f(el) ? el : uptil(el.parentNode, f) +} function commentClick(e) { e = e || window.event; var targ = e.currentTarget || e.srcElement || e; if (targ.nodeType == 3) targ = targ.parentNode; if (e.target.name=="submit") { e.preventDefault() - var form = e.target.parentNode + var form = uptil(e.target, function(el){ return el.tagName == "FORM" }) if (form) { data = new FormData(form) + data.set(e.target.name, e.target.value) + data.set("xhr", 1) if (("c"+data.get("commentid")) == targ.id) { - + targ.action = form.action + if (e.target.value == "preview") { + targ = form + } + console.log("ok") } else if (("c"+data.get("parentid")) == targ.id) { targ = form } else { return } - params = new URLSearchParams(data).toString() - params += "&" + e.target.name + "=" + e.target.value - params += "&xhr=1" e.target.disabled = "disabled" - request(targ.target || "", params, + request(targ.action || "", data, function(res){ targ.outerHTML = res setup() @@ -145,7 +149,7 @@ function loadMoreComments(e) { e.target.parentNode.outerHTML = res + '
load more comments
' setup() } else { - e.target.parentNode.outerHTML = "" + e.target.parentNode.innerHTML = "" } }, function() { e.target.innerHTML = "loading failed" @@ -217,12 +221,15 @@ function formSubmit(e) { var targ = e.currentTarget || e.srcElement || e; e.preventDefault() var data = new FormData(targ) - params = new URLSearchParams(data).toString() - params += "&" + e.submitter.name + "=" + e.submitter.value - params += "&xhr=1" + data.set(e.submitter.name, e.submitter.value) + data.set("xhr", "1") e.submitter.disabled = "disabled" - request(targ.target, params, + request(targ.target, data, function(res){ + if (data.get("op") == "read_post") { + document.getElementById("p"+data.get("postid")).remove() + return + } targ.outerHTML = res setup() }, @@ -233,9 +240,33 @@ function formSubmit(e) { 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 = "
loading
" + request(e.target.href + "&xhr=1", "", function(res) { + mycommunities.innerHTML = '
view all »' + mycommunities.innerHTML += res + }, function() { + mycommunities.className = "" + }) + } + return false +} + function openSettings(e) { e.preventDefault() var settings = document.getElementById("settingspopup") + if (settings.className == "open") { + settings.className = "" + return false + } settings.className = "open" request(e.target.href + "?xhr=1", "", function(res) { settings.innerHTML = res @@ -265,8 +296,7 @@ function saveSettings(e) { var targ = e.currentTarget || e.srcElement || e; var data = new FormData(targ) e.preventDefault() - var params = new URLSearchParams(data).toString() - request(targ.target, params, function(res) { + request(targ.target, data, function(res) { ["endlessScrolling", "autoLoad"].map(function(x) { 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() { if (showimages = document.getElementById("se")) { showimages.addEventListener("click", showImages) @@ -328,6 +368,9 @@ function setup() { if (settings = document.getElementById("opensettings")) { settings.addEventListener("click", openSettings) } + if (settings = document.getElementById("openmycommunities")) { + settings.addEventListener("click", toggleMyCommunities) + } if (hidechildren = document.getElementById("hidechildren")){ hidechildren.addEventListener("click", hideAllChildComments) } @@ -338,6 +381,10 @@ function setup() { } lmc.addEventListener("click", loadMoreComments) } + var imgUpload = document.getElementsByClassName("imgupload") + for (var i = 0; i < imgUpload.length; i++) { + imgUpload[i].addEventListener("change", insertImg) + } var posts = document.getElementsByClassName("post") for (var i = 0; i < posts.length; i++) { posts[i].addEventListener("click", postClick) diff --git a/public/ws.js b/public/ws.js new file mode 100644 index 0000000..08ac7c3 --- /dev/null +++ b/public/ws.js @@ -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() +} diff --git a/routes.go b/routes.go index 1507480..d72d653 100644 --- a/routes.go +++ b/routes.go @@ -32,12 +32,12 @@ var funcMap = template.FuncMap{ } return host }, - "proxy": func(s string) string { + "localize": func(s string) string { u, err := url.Parse(s) if err != nil { return s } - return "/" + u.Host + u.Path + return "." + u.Path + "@" + u.Host }, "printer": func(n any) string { p := message.NewPrinter(language.English) @@ -119,9 +119,9 @@ var funcMap = template.FuncMap{ if re.MatchString(p.URL.String()) { return p.URL.String() + "?format=jpg&thumbnail=96" } - re = regexp.MustCompile(`^https:\/\/i.imgur.com\/([a-zA-Z0-9]+)\.([a-z]+)$`) + re = regexp.MustCompile(`^https:\/\/(i\.)?imgur.com\/([a-zA-Z0-9]{5,})(\.[a-zA-Z0-9]+)?`) if re.MatchString(p.URL.String()) { - return re.ReplaceAllString(p.URL.String(), "https://i.imgur.com/${1}s.$2") + return re.ReplaceAllString(p.URL.String(), "https://i.imgur.com/${2}s.jpg") } if p.URL.IsValid() { return "/_/static/link.png" @@ -133,23 +133,17 @@ var funcMap = template.FuncMap{ var buf bytes.Buffer re := regexp.MustCompile(`\s---\s`) body = re.ReplaceAllString(body, "\n***\n") + // community bangs + body = RegReplace(body, `([^\[])!([a-zA-Z0-9_]+)@([a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)+)`, `$1[!$2@$3](/c/$2@$3)`) if err := md.Convert([]byte(body), &buf); err != nil { fmt.Println(err) return template.HTML(body) } - converted := buf.String() - converted = strings.Replace(converted, `!$1@$2 `) - re = regexp.MustCompile(`::: spoiler (.*?)\n([\S\s]*?):::`) - converted = re.ReplaceAllString(converted, "
$1$2
") - return template.HTML(converted) + body = buf.String() + body = strings.Replace(body, `$1$2") + return template.HTML(body) }, "rmmarkdown": func(body string) string { var buf bytes.Buffer @@ -158,7 +152,7 @@ var funcMap = template.FuncMap{ return body } text := html2text.HTML2TextWithOptions(buf.String(), html2text.WithLinksInnerText()) - re := regexp.MustCompile(`\`) + re := regexp.MustCompile(`\<(https?:\/\/|mailto)(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)\>`) return re.ReplaceAllString(text, "") }, "contains": strings.Contains, @@ -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) { state := State{ - Host: Host, - Page: 1, - Status: http.StatusOK, + Host: Host, + Page: 1, + Status: http.StatusOK, + Version: version, + } + if watch != nil { + state.Watch = *watch } lemmyDomain := os.Getenv("LEMMY_DOMAIN") if lemmyDomain != "" { @@ -209,14 +259,28 @@ func Initialize(Host string, r *http.Request) (State, error) { } state.Listing = getCookie(r, "DefaultListingType") state.Sort = getCookie(r, "DefaultSortType") - state.Dark = getCookie(r, "Dark") != "" + state.CommentSort = getCookie(r, "DefaultCommentSortType") + if dark := getCookie(r, "Dark"); dark != "" { + state.Dark = dark != "0" + } else { + state.Dark = os.Getenv("DARK") != "" + } state.ShowNSFW = getCookie(r, "ShowNSFW") != "" + state.HideInstanceNames = getCookie(r, "HideInstanceNames") != "" + if hide := getCookie(r, "HideThumbnails"); hide != "" { + state.HideThumbnails = hide != "0" + } else { + state.HideThumbnails = os.Getenv("HIDE_THUMBNAILS") != "" + } state.ParseQuery(r.URL.RawQuery) if state.Sort == "" { - state.Sort = "Hot" + state.Sort = getenv("SORT", "Hot") + } + if state.CommentSort == "" { + state.CommentSort = getenv("COMMENT_SORT", "Hot") } if state.Listing == "" || state.Session == nil && state.Listing == "Subscribed" { - state.Listing = "All" + state.Listing = getenv("LISTING", "All") } return state, nil } @@ -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) { state, err := Initialize(ps.ByName("host"), r) if err != nil { Render(w, "index.html", state) return } + if path := strings.Split(ps.ByName("postid"), "@"); len(path) > 1 { + apid := ResolveId(r, "post", path[0], path[1]) + if apid != "" { + resp, err := state.Client.ResolveObject(context.Background(), types.ResolveObject{ + Q: apid, + }) + if err != nil { + dest := apid + if os.Getenv("LEMMY_DOMAIN") == "" { + dest = RegReplace(dest, `https:\/\/([a-zA-Z0-9\.\-]+\/post\/\d+)`, `/$1`) + } + http.Redirect(w, r, dest, 302) + return + } + post, _ := resp.Post.Value() + if post.Post.ID > 0 { + dest := RegReplace(r.URL.String(), `(([a-zA-Z0-9\.\-]+)?/post/)([a-zA-Z0-9\-\.@]+)`, `$1`) + dest += strconv.Itoa(post.Post.ID) + http.Redirect(w, r, dest, 302) + return + } else { + http.Redirect(w, r, apid, 302) + return + } + } + } m, _ := url.ParseQuery(r.URL.RawQuery) if len(m["edit"]) > 0 { state.Op = "edit_post" state.GetSite() } + if len(m["content"]) > 0 { + state.Content = m["content"][0] + } postid, _ := strconv.Atoi(ps.ByName("postid")) state.GetPost(postid) state.GetComments() @@ -407,6 +538,32 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { Render(w, "index.html", state) return } + if path := strings.Split(ps.ByName("commentid"), "@"); len(path) > 1 { + apid := ResolveId(r, "comment", path[0], path[1]) + if apid != "" { + resp, err := state.Client.ResolveObject(context.Background(), types.ResolveObject{ + Q: apid, + }) + if err != nil { + dest := apid + if os.Getenv("LEMMY_DOMAIN") == "" { + dest = RegReplace(dest, `https:\/\/([a-zA-Z0-9\.\-]+\/comment\/\d+)`, `/$1`) + } + http.Redirect(w, r, dest, 302) + return + } + comment, _ := resp.Comment.Value() + if comment.Comment.ID > 0 { + dest := RegReplace(r.URL.String(), `(([a-zA-Z0-9\.\-]+)?/comment/)([a-zA-Z0-9\-\.@]+)`, `$1`) + dest += strconv.Itoa(comment.Comment.ID) + http.Redirect(w, r, dest, 302) + return + } else { + http.Redirect(w, r, apid, 302) + return + } + } + } m, _ := url.ParseQuery(r.URL.RawQuery) if len(m["reply"]) > 0 { state.Op = "reply" @@ -414,6 +571,9 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { if len(m["edit"]) > 0 { state.Op = "edit" } + if r.Method == "POST" && len(m["content"]) > 0 { + state.Content = m["content"][0] + } if len(m["source"]) > 0 { state.Op = "source" } @@ -423,6 +583,10 @@ func GetComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { } commentid, _ := strconv.Atoi(ps.ByName("commentid")) state.GetComment(commentid) + if state.XHR && len(m["content"]) > 0 { + Render(w, "create_comment.html", state) + return + } state.GetPost(state.PostID) Render(w, "index.html", state) } @@ -540,9 +704,10 @@ func Settings(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { Render(w, "index.html", state) return } + state.GetSite() switch r.Method { case "POST": - for _, name := range []string{"DefaultSortType", "DefaultListingType"} { + for _, name := range []string{"DefaultSortType", "DefaultListingType", "DefaultCommentSortType"} { deleteCookie(w, state.Host, 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") state.Dark = true } else { - deleteCookie(w, state.Host, "Dark") - deleteCookie(w, "", "Dark") + setCookie(w, "", "Dark", "0") state.Dark = false } if r.FormValue("shownsfw") != "" { @@ -562,8 +726,23 @@ func Settings(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { deleteCookie(w, "", "ShowNSFW") state.ShowNSFW = false } + if r.FormValue("hideInstanceNames") != "" { + setCookie(w, "", "HideInstanceNames", "1") + state.HideInstanceNames = true + } else { + deleteCookie(w, "", "HideInstanceNames") + state.HideInstanceNames = false + } + if r.FormValue("hideThumbnails") != "" { + setCookie(w, "", "HideThumbnails", "1") + state.HideInstanceNames = true + } else { + setCookie(w, "", "HideThumbnails", "0") + state.HideInstanceNames = false + } state.Listing = r.FormValue("DefaultListingType") state.Sort = r.FormValue("DefaultSortType") + state.CommentSort = r.FormValue("DefaultCommentSortType") // TODO save user settings case "GET": if state.Session != nil { @@ -740,6 +919,18 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { CommunityID: communityid, Follow: true, }) + case "block": + communityid, _ := strconv.Atoi(r.FormValue("communityid")) + state.Client.BlockCommunity(context.Background(), types.BlockCommunity{ + CommunityID: communityid, + Block: true, + }) + case "unblock": + communityid, _ := strconv.Atoi(r.FormValue("communityid")) + state.Client.BlockCommunity(context.Background(), types.BlockCommunity{ + CommunityID: communityid, + Block: false, + }) case "logout": deleteCookie(w, state.Host, "jwt") deleteCookie(w, state.Host, "user") @@ -755,10 +946,15 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { if err != nil { if strings.Contains(fmt.Sprintf("%v", err), "missing_totp_token") { state.Op = "2fa" - Render(w, "login.html", state) - return + } + state.GetSite() + if state.Site != nil && state.Site.SiteView.LocalSite.CaptchaEnabled { + state.GetCaptcha() } state.Status = http.StatusUnauthorized + state.Error = err + Render(w, "login.html", state) + return } else if resp.JWT.IsValid() { state.GetUser(r.FormValue("username")) if state.User != nil { @@ -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.RawQuery = "" } + case "read_post": + postid, _ := strconv.Atoi(r.FormValue("postid")) + post := types.MarkPostAsRead{ + PostID: postid, + Read: true, + } + if r.FormValue("submit") == "mark unread" { + post.Read = false + } + _, err := state.Client.MarkPostAsRead(context.Background(), post) + if err != nil { + fmt.Println(err) + } else if r.FormValue("xhr") != "" { + w.Write([]byte{}) + return + } case "vote_post": var score int16 score = 1 @@ -1023,9 +1235,20 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { parentid, _ := strconv.Atoi(r.FormValue("parentid")) state.GetComment(parentid) } + content := r.FormValue("content") + file, handler, err := r.FormFile("file") + if err == nil { + pres, err := state.UploadImage(file, handler) + if err != nil { + state.Error = err + Render(w, "index.html", state) + return + } + content += ("![](https://" + state.Host + "/pictrs/image/" + pres.Files[0].Filename + ")") + } if r.FormValue("submit") == "save" { createComment := types.CreateComment{ - Content: r.FormValue("content"), + Content: content, PostID: state.PostID, } if state.CommentID > 0 { @@ -1047,6 +1270,22 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { } else { fmt.Println(err) } + } else if r.FormValue("submit") == "preview" { + q := r.URL.Query() + q.Set("content", content) + q.Set("reply", "") + if r.FormValue("xhr") != "" { + q.Set("xhr", "1") + } + r.URL.RawQuery = q.Encode() + if ps.ByName("postid") != "" { + GetPost(w, r, ps) + return + } + if ps.ByName("commentid") != "" { + GetComment(w, r, ps) + return + } } else if r.FormValue("xhr") != "" { w.Write([]byte{}) return @@ -1056,10 +1295,23 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { } case "edit_comment": commentid, _ := strconv.Atoi(r.FormValue("commentid")) + q := r.URL.Query() + content := r.FormValue("content") + file, handler, err := r.FormFile("file") + if err == nil { + pres, err := state.UploadImage(file, handler) + if err != nil { + state.Error = err + Render(w, "index.html", state) + return + } + content += ("![](https://" + state.Host + "/pictrs/image/" + pres.Files[0].Filename + ")") + } + if r.FormValue("submit") == "save" { resp, err := state.Client.EditComment(context.Background(), types.EditComment{ CommentID: commentid, - Content: types.NewOptional(r.FormValue("content")), + Content: types.NewOptional(content), }) if err != nil { fmt.Println(err) @@ -1068,6 +1320,29 @@ func UserOp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { r.URL.Fragment = "c" + commentid r.URL.RawQuery = "" } + } else if r.FormValue("submit") == "preview" { + q.Set("content", content) + q.Set("edit", "") + if r.FormValue("xhr") != "" { + q.Set("xhr", "1") + } + r.URL.RawQuery = q.Encode() + if ps.ByName("commentid") != "" { + GetComment(w, r, ps) + return + } + } else if r.FormValue("submit") == "cancel" { + if ps.ByName("commentid") != "" { + if r.FormValue("xhr") != "" { + q.Set("xhr", "1") + } + r.URL.RawQuery = q.Encode() + GetComment(w, r, ps) + return + } + } else if r.FormValue("xhr") != "" { + w.Write([]byte{}) + return } if r.FormValue("xhr") != "" { state.XHR = true @@ -1136,6 +1411,7 @@ func GetRouter() *httprouter.Router { router.POST("/:host/create_post", middleware(UserOp)) router.GET("/:host/create_community", middleware(GetCreateCommunity)) router.POST("/:host/create_community", middleware(UserOp)) + router.GET("/:host/communities", middleware(GetCommunities)) } else { router.ServeFiles("/_/static/*filepath", http.Dir("public")) router.GET("/", middleware(GetFrontpage)) @@ -1167,6 +1443,7 @@ func GetRouter() *httprouter.Router { router.POST("/create_post", middleware(UserOp)) router.GET("/create_community", middleware(GetCreateCommunity)) router.POST("/create_community", middleware(UserOp)) + router.GET("/communities", middleware(GetCommunities)) } return router } diff --git a/state.go b/state.go index 266eddc..1e2085a 100644 --- a/state.go +++ b/state.go @@ -11,6 +11,8 @@ import ( "mime/multipart" "net/http" "net/url" + "os" + "regexp" "sort" "strconv" "strings" @@ -57,47 +59,85 @@ type Post struct { } type Session struct { - UserName string - UserID int + UserName string + UserID int + Communities []types.CommunityView } type State struct { - Client *lemmy.Client - HTTPClient *http.Client - Session *Session - Status int - Error error - Alert string - Host string - CommunityName string - Community *types.GetCommunityResponse - TopCommunities []types.CommunityView - Communities []types.CommunityView - UnreadCount int64 - Sort string - Listing string - Page int - Parts []string - Posts []Post - Comments []Comment - Activities []Activity - CommentCount int - PostID int - CommentID int - Context int - UserName string - User *types.GetPersonDetailsResponse - Now int64 - XHR bool - Op string - Site *types.GetSiteResponse - Query string - SearchType string - Captcha *types.CaptchaResponse - Dark bool - ShowNSFW bool + Watch bool + Version string + Client *lemmy.Client + HTTPClient *http.Client + Session *Session + Status int + Error error + Alert string + Host string + CommunityName string + Community *types.GetCommunityResponse + TopCommunities []types.CommunityView + Communities []types.CommunityView + UnreadCount int64 + Sort string + CommentSort string + Listing string + Page int + Parts []string + Posts []Post + Comments []Comment + Activities []Activity + CommentCount int + PostID int + CommentID int + Context int + UserName string + User *types.GetPersonDetailsResponse + Now int64 + XHR bool + Op string + Site *types.GetSiteResponse + Query string + Content string + SearchType string + Captcha *types.CaptchaResponse + Dark bool + ShowNSFW bool + HideInstanceNames bool + HideThumbnails bool } +func (s State) Unknown() string { + fmt.Println(fmt.Sprintf("%v", s.Error)) + re := regexp.MustCompile(`(.*?)@(.*?)@`) + if strings.Contains(fmt.Sprintf("%v", s.Error), "couldnt_find_community") { + matches := re.FindAllStringSubmatch(s.CommunityName+"@", -1) + if len(matches) < 1 || len(matches[0]) < 3 { + return "" + } + if matches[0][2] != s.Host { + remote := "/" + matches[0][2] + "/c/" + matches[0][1] + if os.Getenv("LEMMY_DOMAIN") != "" { + remote = "https:/" + remote + } + return remote + } + } + if strings.Contains(fmt.Sprintf("%v", s.Error), "couldnt_find_that_username_or_email") { + matches := re.FindAllStringSubmatch(s.UserName+"@", -1) + if len(matches) < 1 || len(matches[0]) < 3 { + return "" + } + if matches[0][2] != s.Host { + remote := "/" + matches[0][2] + "/u/" + matches[0][1] + if os.Getenv("LEMMY_DOMAIN") != "" { + remote = "https:/" + remote + } + return remote + } + } + return "" +} func (p State) SortBy(v string) string { var q string if p.Query != "" || p.SearchType == "Communities" { @@ -163,6 +203,7 @@ func (state *State) ParseQuery(RawQuery string) { } if len(m["sort"]) > 0 { state.Sort = m["sort"][0] + state.CommentSort = m["sort"][0] } if len(m["communityname"]) > 0 { state.CommunityName = m["communityname"][0] @@ -221,17 +262,27 @@ func (state *State) GetCaptcha() { } } func (state *State) GetSite() { - token := state.Client.Token - state.Client.Token = "" resp, err := state.Client.Site(context.Background(), types.GetSite{}) if err != nil { + fmt.Println(err) state.Status = http.StatusInternalServerError state.Host = "." - state.Error = errors.New("site unreachable") + state.Error = errors.New("unable to retrieve site") return } - state.Client.Token = token state.Site = resp + if !state.Site.MyUser.IsValid() { + return + } + for _, c := range state.Site.MyUser.MustValue().Follows { + state.Session.Communities = append(state.Session.Communities, types.CommunityView{ + Community: c.Community, + Subscribed: "Subscribed", + }) + } + sort.Slice(state.Session.Communities, func(a, b int) bool { + return state.Session.Communities[a].Community.Name < state.Session.Communities[b].Community.Name + }) } func (state *State) GetComment(commentid int) { @@ -241,7 +292,7 @@ func (state *State) GetComment(commentid int) { state.CommentID = commentid cresp, err := state.Client.Comments(context.Background(), types.GetComments{ ParentID: types.NewOptional(state.CommentID), - Sort: types.NewOptional(types.CommentSortType(state.Sort)), + Sort: types.NewOptional(types.CommentSortType(state.CommentSort)), Type: types.NewOptional(types.ListingType("All")), Limit: types.NewOptional(int64(50)), }) @@ -265,6 +316,9 @@ func (state *State) GetComment(commentid int) { state.Comments = append(state.Comments, comment) } } + if len(state.Comments) == 0 { + return + } ctx, err := state.GetContext(state.Context, state.Comments[0]) if err != nil { fmt.Println(err) @@ -296,7 +350,7 @@ func (state *State) GetComments() { } cresp, err := state.Client.Comments(context.Background(), types.GetComments{ PostID: types.NewOptional(state.PostID), - Sort: types.NewOptional(types.CommentSortType(state.Sort)), + Sort: types.NewOptional(types.CommentSortType(state.CommentSort)), Type: types.NewOptional(types.ListingType("All")), Limit: types.NewOptional(int64(50)), Page: types.NewOptional(int64(state.Page)), @@ -408,6 +462,7 @@ func (state *State) GetUser(username string) { }) if err != nil { fmt.Println(err) + state.Error = err state.Status = http.StatusInternalServerError return } @@ -492,7 +547,18 @@ func (state *State) GetPosts() { func (state *State) Search(searchtype string) { if state.Query == "" && searchtype == "Communities" { + if state.Listing == "Subscribed" { + if state.Page > 1 { + return + } + if state.Site == nil { + state.GetSite() + } + state.Communities = state.Session.Communities + return + } resp, err := state.Client.Communities(context.Background(), types.ListCommunities{ + Type: types.NewOptional(types.ListingType(state.Listing)), Sort: types.NewOptional(types.SortType(state.Sort)), Limit: types.NewOptional(int64(25)), Page: types.NewOptional(int64(state.Page)), @@ -507,7 +573,7 @@ func (state *State) Search(searchtype string) { search := types.Search{ Q: state.Query, Sort: types.NewOptional(types.SortType(state.Sort)), - ListingType: types.NewOptional(types.ListingType("All")), + ListingType: types.NewOptional(types.ListingType(state.Listing)), Type: types.NewOptional(types.SearchType(searchtype)), Limit: types.NewOptional(int64(25)), Page: types.NewOptional(int64(state.Page)), @@ -559,22 +625,22 @@ func (state *State) GetPost(postid int) { }) if err != nil { state.Status = http.StatusInternalServerError + state.Error = err return - } else { - state.Posts = []Post{Post{ - PostView: resp.PostView, - State: state, - }} - 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) { diff --git a/templates/activities.html b/templates/activities.html index 12b3284..110419b 100644 --- a/templates/activities.html +++ b/templates/activities.html @@ -13,7 +13,12 @@ {{$state.User.PersonView.Person.Name }} {{ end }} in - c/{{ $activity.Comment.P.Community.Name }} + + c/{{ if $state.HideInstanceNames -}} + {{ $activity.Comment.P.Community.Name }} + {{ else -}} + {{ fullcname $activity.Comment.P.Community }} + {{ end }}
{{ template "comment.html" $activity.Comment }} @@ -25,10 +30,22 @@ message {{ if eq $activity.Message.Creator.ID $state.Session.UserID }} to - {{ $activity.Message.Recipient.Name }} + + {{- if $state.HideInstanceNames -}} + {{ $activity.Message.Recipient.Name }} + {{- else -}} + {{ fullname $activity.Message.Recipient }} + {{- end -}} + {{ else }} from - {{ $activity.Message.Creator.Name }} + + {{- if $state.HideInstanceNames -}} + {{ $activity.Message.Creator.Name }} + {{- else -}} + {{ fullname $activity.Message.Creator }} + {{- end -}} + {{end}} sent {{ humanize $activity.Message.PrivateMessage.Published.Time }} diff --git a/templates/comment.html b/templates/comment.html index 2a740ea..c33d77a 100644 --- a/templates/comment.html +++ b/templates/comment.html @@ -1,4 +1,4 @@ -
+
{{ if .State.Session }}
+
diff --git a/templates/community.html b/templates/community.html index f1af3c6..287e868 100644 --- a/templates/community.html +++ b/templates/community.html @@ -1,17 +1,17 @@
- +
- c/{{fullcname .Community}}: {{.Community.Title}} + c/{{fullcname .Community}}: {{.Community.Title}}
{{ if .Community.Description.IsValid }}
- {{markdown "poop" .Community.Description.String}} + {{ markdown "" .Community.Description.String }}
{{ end }}
- {{printer .Counts.Subscribers}} subscribers, + {{ if .Counts.Subscribers }}{{ printer .Counts.Subscribers }} subscribers,{{end}} a community founded {{ humanize .Community.Published.Time }}
diff --git a/templates/create_comment.html b/templates/create_comment.html new file mode 100644 index 0000000..081d8eb --- /dev/null +++ b/templates/create_comment.html @@ -0,0 +1,45 @@ +
+
+ +
+
+ +
+ {{ if eq .Op "edit" }} + + + {{ else }} + + {{ end }} + + + {{ if or .Op .Content }} + + {{ end }} + + {{ if .Content }} +
+
+

Preview

+
+ {{ markdown .Host .Content }} +
+
+
+ {{ end }} +
diff --git a/templates/frontpage.html b/templates/frontpage.html index ea74320..bce44e6 100644 --- a/templates/frontpage.html +++ b/templates/frontpage.html @@ -2,26 +2,12 @@ {{ 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}} - + {{ template "nav.html" . -}} @@ -29,12 +15,15 @@ {{ template "nsfw.html" }} {{ else }}
- {{ if or (contains .Sort "Top") (and (not .PostID) (not .User) (not .Community) (not .Activities) (eq .Op ""))}} - {{ template "menu.html" . }} - {{ end}} + {{ template "menu.html" . }} {{ if .Error }} -
{{.Error}}
+
+ {{.Error}}. + {{ if .Unknown }} + try remote instance: {{ .Unknown }} + {{ end }} +
{{ end }} {{ range .Posts }} @@ -56,6 +45,6 @@ {{ template "sidebar.html" . }}
{{ end }} - + diff --git a/templates/login.html b/templates/login.html index d310403..2ecb7bd 100644 --- a/templates/login.html +++ b/templates/login.html @@ -2,7 +2,7 @@ {{ host .Host }}: sign up or log in - + @@ -38,7 +38,7 @@ {{ if ne .Op "2fa" }}

create a new account

-
+