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).

-### 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 + '
'
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 += ("")
+ }
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 += ("")
+ }
+
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 @@
-