style the join (claim invite) pages
This commit is contained in:
parent
371109c136
commit
08c8b0cced
docs
web
assets
handlers
i18n/defaults
router
templates
BIN
docs/invites-chart.png
Normal file
BIN
docs/invites-chart.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 62 KiB |
9
docs/invites.md
Normal file
9
docs/invites.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Invites
|
||||
|
||||
This implementation of Rooms 2.0 is compliant with the [Rooms 2.0 specification](https://github.com/ssb-ngi-pointer/rooms2), but we add a few additional features and pages in order to improve user experience when their SSB app does not support [SSB URIs](https://github.com/ssb-ngi-pointer/ssb-uri-spec).
|
||||
|
||||
A summary can be seen in the following chart:
|
||||
|
||||

|
||||
|
||||
When the browser and operating system detects no support for opening SSB URIs, we redirect to a fallback page which presents the user with two broad options: (1) install an SSB app that supports SSB URIs, (2) link to another page where the user can manually input the user's SSB ID in a form.
|
17
web/assets/autoredirect.js
Normal file
17
web/assets/autoredirect.js
Normal file
@ -0,0 +1,17 @@
|
||||
let hasFocus = true;
|
||||
window.addEventListener('blur', () => {
|
||||
hasFocus = false;
|
||||
});
|
||||
window.addEventListener('focus', () => {
|
||||
hasFocus = true;
|
||||
});
|
||||
|
||||
const anchorElem = document.getElementById('join-room-uri');
|
||||
anchorElem.onclick = function handleURI(ev) {
|
||||
const ssbUri = ev.target.dataset.href;
|
||||
const fallbackUrl = ev.target.dataset.hrefFallback;
|
||||
setTimeout(function () {
|
||||
if (hasFocus) window.location = fallbackUrl;
|
||||
}, 500);
|
||||
window.location = ssbUri;
|
||||
};
|
@ -39,6 +39,8 @@ var HTMLTemplates = []string{
|
||||
|
||||
"invite/consumed.tmpl",
|
||||
"invite/facade.tmpl",
|
||||
"invite/facade-fallback.tmpl",
|
||||
"invite/insert-id.tmpl",
|
||||
|
||||
"notice/list.tmpl",
|
||||
"notice/show.tmpl",
|
||||
@ -301,11 +303,14 @@ func New(
|
||||
var ih = inviteHandler{
|
||||
render: r,
|
||||
|
||||
invites: dbs.Invites,
|
||||
invites: dbs.Invites,
|
||||
pinnedNotices: dbs.PinnedNotices,
|
||||
|
||||
networkInfo: netInfo,
|
||||
}
|
||||
m.Get(router.CompleteInviteFacade).Handler(r.HTML("invite/facade.tmpl", ih.presentFacade))
|
||||
m.Get(router.CompleteInviteFacadeFallback).Handler(r.HTML("invite/facade-fallback.tmpl", ih.presentFacadeFallback))
|
||||
m.Get(router.CompleteInviteInsertID).Handler(r.HTML("invite/insert-id.tmpl", ih.presentInsert))
|
||||
m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume)
|
||||
|
||||
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
|
||||
|
@ -1,15 +1,18 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"image/color"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"go.mindeco.de/http/render"
|
||||
"go.mindeco.de/logging"
|
||||
|
||||
@ -24,22 +27,13 @@ import (
|
||||
type inviteHandler struct {
|
||||
render *render.Renderer
|
||||
|
||||
invites roomdb.InvitesService
|
||||
invites roomdb.InvitesService
|
||||
pinnedNotices roomdb.PinnedNoticesService
|
||||
|
||||
networkInfo network.ServerEndpointDetails
|
||||
}
|
||||
|
||||
func (h inviteHandler) presentFacade(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
token := req.URL.Query().Get("token")
|
||||
|
||||
inv, err := h.invites.GetByToken(req.Context(), token)
|
||||
if err != nil {
|
||||
if errors.Is(err, roomdb.ErrNotFound) {
|
||||
return nil, weberrors.ErrNotFound{What: "invite"}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func buildJoinRoomURI(h inviteHandler, token string) string {
|
||||
var joinRoomURI url.URL
|
||||
joinRoomURI.Scheme = "ssb"
|
||||
joinRoomURI.Opaque = "experimental"
|
||||
@ -60,13 +54,88 @@ func (h inviteHandler) presentFacade(rw http.ResponseWriter, req *http.Request)
|
||||
|
||||
joinRoomURI.RawQuery = queryVals.Encode()
|
||||
|
||||
return joinRoomURI.String()
|
||||
}
|
||||
|
||||
func (h inviteHandler) presentFacade(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
token := req.URL.Query().Get("token")
|
||||
|
||||
_, err := h.invites.GetByToken(req.Context(), token)
|
||||
if err != nil {
|
||||
if errors.Is(err, roomdb.ErrNotFound) {
|
||||
return nil, weberrors.ErrNotFound{What: "invite"}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notice, err := h.pinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find room's description: %w", err)
|
||||
}
|
||||
|
||||
joinRoomURI := buildJoinRoomURI(h, token)
|
||||
|
||||
urlTo := web.NewURLTo(router.CompleteApp())
|
||||
fallbackURL := urlTo(router.CompleteInviteFacadeFallback, "token", token)
|
||||
|
||||
// generate a QR code with the token inside so that you can open it easily in a supporting mobile app
|
||||
qrCode, err := qrcode.New(joinRoomURI, qrcode.Medium)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qrCode.BackgroundColor = color.Transparent // transparent to fit into the page
|
||||
qrCode.ForegroundColor = color.Black
|
||||
|
||||
qrCodeData, err := qrCode.PNG(-5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qrURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCodeData)
|
||||
|
||||
return map[string]interface{}{
|
||||
csrf.TemplateTag: csrf.TemplateField(req),
|
||||
"RoomTitle": notice.Title,
|
||||
"JoinRoomURI": template.URL(joinRoomURI),
|
||||
"FallbackURL": fallbackURL,
|
||||
"QRCodeURI": template.URL(qrURI),
|
||||
}, nil
|
||||
}
|
||||
|
||||
"Invite": inv,
|
||||
"Token": token,
|
||||
func (h inviteHandler) presentFacadeFallback(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
token := req.URL.Query().Get("token")
|
||||
|
||||
"JoinRoomURI": template.URL(joinRoomURI.String()),
|
||||
_, err := h.invites.GetByToken(req.Context(), token)
|
||||
if err != nil {
|
||||
if errors.Is(err, roomdb.ErrNotFound) {
|
||||
return nil, weberrors.ErrNotFound{What: "invite"}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
urlTo := web.NewURLTo(router.CompleteApp())
|
||||
insertURL := urlTo(router.CompleteInviteInsertID, "token", token)
|
||||
|
||||
return map[string]interface{}{
|
||||
csrf.TemplateTag: csrf.TemplateField(req),
|
||||
"InsertURL": insertURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h inviteHandler) presentInsert(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
token := req.URL.Query().Get("token")
|
||||
|
||||
_, err := h.invites.GetByToken(req.Context(), token)
|
||||
if err != nil {
|
||||
if errors.Is(err, roomdb.ErrNotFound) {
|
||||
return nil, weberrors.ErrNotFound{What: "invite"}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
csrf.TemplateTag: csrf.TemplateField(req),
|
||||
"Token": token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -213,22 +282,9 @@ func (html *inviteConsumeHTMLResponder) UpdateMultiserverAddr(msaddr string) {
|
||||
}
|
||||
|
||||
func (html inviteConsumeHTMLResponder) SendSuccess() {
|
||||
|
||||
// construct the ssb:experimental?action=consume-invite&... uri for linking into apps
|
||||
queryParams := url.Values{}
|
||||
queryParams.Set("action", "join-room")
|
||||
queryParams.Set("multiserverAddress", html.multiservAddr)
|
||||
|
||||
// html.multiservAddr
|
||||
ssbURI := url.URL{
|
||||
Scheme: "ssb",
|
||||
Opaque: "experimental",
|
||||
RawQuery: queryParams.Encode(),
|
||||
}
|
||||
|
||||
err := html.renderer.Render(html.rw, html.req, "invite/consumed.tmpl", http.StatusOK, struct {
|
||||
SSBURI template.URL
|
||||
}{template.URL(ssbURI.String())})
|
||||
MultiserverAddress string
|
||||
}{(html.multiservAddr)})
|
||||
if err != nil {
|
||||
logger := logging.FromContext(html.req.Context())
|
||||
level.Warn(logger).Log("event", "render failed", "err", err)
|
||||
|
@ -2,12 +2,15 @@ GenericConfirm = "Yes"
|
||||
GenericGoBack = "Back"
|
||||
GenericSave = "Save"
|
||||
GenericCreate = "Create"
|
||||
GenericSubmit = "Submit"
|
||||
GenericPreview = "Preview"
|
||||
GenericLanguage = "Language"
|
||||
GenericOpenLink = "Open Link"
|
||||
|
||||
PageNotFound = "The requested page was not found."
|
||||
|
||||
PubKeyRefPlaceholder = "@ .ed25519"
|
||||
|
||||
RoleMember = "Member"
|
||||
RoleModerator = "Moderator"
|
||||
RoleAdmin = "Admin"
|
||||
@ -82,12 +85,18 @@ NavAdminNotices = "Notices"
|
||||
|
||||
InviteFacade = "Join Room"
|
||||
InviteFacadeTitle = "Join Room"
|
||||
InviteFacadeWelcome = "elaborate welcome message for a new member with good words and stuff."
|
||||
InviteFacadeAliasSuggestion = "The persone who created thought you might like this alias:"
|
||||
InviteFacadePublicKey = "Public Key"
|
||||
InviteFacadeWelcome = "You have permission to become a member of this room because someone has shared this invite with you."
|
||||
InviteFacadeInstruct = "To claim the invite, press the button below which will open a compatible SSB app, if it's installed."
|
||||
InviteFacadeJoin = "Join this room"
|
||||
|
||||
InviteFacadeFallbackWelcome = "Are you new to SSB? It seems you don't have an SSB app capable of understanding that link. You can install one of these apps:"
|
||||
InviteFacadeFallbackManyverse = "Install Manyverse"
|
||||
InviteFacadeFallbackInsertID = "Insert SSB ID"
|
||||
|
||||
InviteInsertWelcome = "You can claim your invite by inserting your SSB ID below. After that, you'll be able to connect to the room in your SSB app."
|
||||
|
||||
InviteConsumedTitle = "Invite accepted!"
|
||||
InviteConsumedWelcome = "Even more elaborate message that the person is now a member of the room!"
|
||||
InviteConsumedWelcome = "You are now a member of this room. If you need a multiserver address to connect to the room, you can copy-paste the one below:"
|
||||
|
||||
NoticeEditTitle = "Edit Notice"
|
||||
NoticeList = "Notices"
|
||||
|
@ -16,8 +16,10 @@ const (
|
||||
|
||||
CompleteAliasResolve = "complete:alias:resolve"
|
||||
|
||||
CompleteInviteFacade = "complete:invite:accept"
|
||||
CompleteInviteConsume = "complete:invite:consume"
|
||||
CompleteInviteFacade = "complete:invite:accept"
|
||||
CompleteInviteFacadeFallback = "complete:invite:accept:fallback"
|
||||
CompleteInviteInsertID = "complete:invite:insert-id"
|
||||
CompleteInviteConsume = "complete:invite:consume"
|
||||
)
|
||||
|
||||
// CompleteApp constructs a mux.Router containing the routes for batch Complete html frontend
|
||||
@ -33,6 +35,8 @@ func CompleteApp() *mux.Router {
|
||||
m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve)
|
||||
|
||||
m.Path("/join").Methods("GET").Name(CompleteInviteFacade)
|
||||
m.Path("/join-fallback").Methods("GET").Name(CompleteInviteFacadeFallback)
|
||||
m.Path("/join-manually").Methods("GET").Name(CompleteInviteInsertID)
|
||||
m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume)
|
||||
|
||||
m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow)
|
||||
|
@ -22,7 +22,7 @@
|
||||
<input
|
||||
type="text"
|
||||
name="pub_key"
|
||||
placeholder="@ .ed25519"
|
||||
placeholder="{{i18n "PubKeyRefPlaceholder"}}"
|
||||
class="font-mono truncate w-1/2 mr-2 tracking-wider h-12 text-gray-900 focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent placeholder-gray-300"
|
||||
>
|
||||
<input
|
||||
|
@ -22,7 +22,7 @@
|
||||
<input
|
||||
type="text"
|
||||
name="pub_key"
|
||||
placeholder="@ .ed25519"
|
||||
placeholder="{{i18n "PubKeyRefPlaceholder"}}"
|
||||
class="font-mono truncate w-1/2 ml-3 tracking-wider h-12 text-gray-900 focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent placeholder-gray-300"
|
||||
>
|
||||
<input
|
||||
|
@ -1,12 +1,17 @@
|
||||
{{ define "title" }}{{i18n "InviteConsumedTitle"}}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col justify-center items-center h-64">
|
||||
<div class="flex flex-col justify-center items-center self-center max-w-lg">
|
||||
<svg class="mt-6 w-32 h-32 text-green-300" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z" />
|
||||
</svg>
|
||||
|
||||
<span
|
||||
id="welcome"
|
||||
class="text-center"
|
||||
>{{i18n "InviteConsumedWelcome"}}</span>
|
||||
class="my-6 text-center"
|
||||
>{{i18n "InviteConsumedTitle"}}<br />{{i18n "InviteConsumedWelcome"}}</span>
|
||||
|
||||
<a id="join-link" href="{{.SSBURI}}">Join Room</a>
|
||||
<span
|
||||
class="bg-gray-200 py-1 px-2 mb-8 w-64 font-mono break-all"
|
||||
>{{.MultiserverAddress}}</span>
|
||||
</div>
|
||||
{{end}}
|
29
web/templates/invite/facade-fallback.tmpl
Normal file
29
web/templates/invite/facade-fallback.tmpl
Normal file
@ -0,0 +1,29 @@
|
||||
{{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col justify-center items-center self-center max-w-lg">
|
||||
<p class="text-center mt-8">{{i18n "InviteFacadeFallbackWelcome"}}</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-center mt-8">
|
||||
<a
|
||||
href="https://manyver.se"
|
||||
class="shadow rounded flex flex-row justify-center items-center px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
|
||||
>{{i18n "InviteFacadeFallbackManyverse"}}</a>
|
||||
|
||||
<a
|
||||
href="https://manyver.se"
|
||||
class="mt-8 sm:ml-4 sm:mt-0 shadow rounded flex flex-row justify-center items-center px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
|
||||
>Install Imaginary</a>
|
||||
</div>
|
||||
|
||||
<hr class="mt-8 w-64 h-px bg-gray-200"></hr>
|
||||
|
||||
<p
|
||||
class="text-center my-8"
|
||||
>Or if you already have an SSB app (such as <a class="text-pink-600 underline" href="https://github.com/ssbc/patchwork" target="_blank">Patchwork</a>, <a class="text-pink-600 underline" href="https://github.com/ssbc/patchbay" target="_blank">Patchbay</a>, <a class="text-pink-600 underline" href="http://git.scuttlebot.io/%25YAg1hicat%2B2GELjE2QJzDwlAWcx0ML%2B1sXEdsWwvdt8%3D.sha256" target="_blank">Patchfoo</a>, <a class="text-pink-600 underline" href="https://github.com/fraction/oasis" target="_blank">Oasis</a>, <a class="text-pink-600 underline" href="https://github.com/planetary-social/planetary-ios" target="_blank">Planetary</a>) and it couldn't process the link for whatever reason, you can manually input your identifier here:</p>
|
||||
|
||||
<a
|
||||
href="{{.InsertURL}}"
|
||||
class="mb-8 shadow rounded flex flex-row justify-center items-center px-4 h-8 text-gray-600 bg-white hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-opacity-50"
|
||||
>{{i18n "InviteFacadeFallbackInsertID"}}</a>
|
||||
</div>
|
||||
{{ end }}
|
@ -1,39 +1,30 @@
|
||||
{{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col justify-center items-center h-64">
|
||||
<div class="flex flex-col justify-center items-center self-center max-w-lg">
|
||||
<p id="welcome" class="text-center mt-8 italic">Welcome to {{.RoomTitle}}! {{i18n "InviteFacadeWelcome"}}</p>
|
||||
<p class="text-center mt-3">{{i18n "InviteFacadeInstruct"}}</p>
|
||||
|
||||
<span
|
||||
id="welcome"
|
||||
class="text-center"
|
||||
>{{ i18n "InviteFacadeWelcome" }}</span>
|
||||
<a
|
||||
id="join-room-uri"
|
||||
href="#"
|
||||
data-href="{{.JoinRoomURI}}"
|
||||
data-href-fallback="{{.FallbackURL}}"
|
||||
class="shadow rounded flex flex-row justify-center items-center mt-8 px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
|
||||
>{{i18n "InviteFacadeJoin"}}</a>
|
||||
|
||||
<a
|
||||
href="{{.JoinRoomURI}}"
|
||||
class="my-8 shadow rounded px-4 h-8 text-gray-100 bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-opacity-50"
|
||||
>Join</a>
|
||||
<hr class="mt-8 w-64 h-px bg-gray-200"></hr>
|
||||
|
||||
<hr class="mb-10 pt-10">
|
||||
<h3 class="text-red-500">TODO: html form fallback / advanced use</h3>
|
||||
<span class="text-center mt-8">{{i18n "AuthWithSSBInstructQR"}}</span>
|
||||
|
||||
<form id="consume" action="{{urlTo "complete:invite:consume"}}" method="POST">
|
||||
{{ .csrfField }}
|
||||
<input type="hidden" name="invite" value={{.Token}}>
|
||||
<img
|
||||
id="start-auth-qrcode"
|
||||
src="{{.QRCodeURI}}"
|
||||
alt="QR-Code to pass the challenge to an App"
|
||||
width="160"
|
||||
height="160"
|
||||
class="mt-8"
|
||||
/>
|
||||
|
||||
|
||||
<div class="my-4 flex flex-row items-center justify-start">
|
||||
<label class="mr-2">{{ i18n "InviteFacadePublicKey" }}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="id"
|
||||
placeholder="@ .ed25519"
|
||||
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:border-transparent">
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="shadow rounded px-4 h-8 text-gray-100 bg-pink-600 hover:bg-pink-700 focus:outline-none focus:ring-2 focus:ring-pink-600 focus:ring-opacity-50"
|
||||
>{{i18n "GenericConfirm"}}</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<script src="/assets/autoredirect.js"></script>
|
||||
</div>
|
||||
{{ end }}
|
27
web/templates/invite/insert-id.tmpl
Normal file
27
web/templates/invite/insert-id.tmpl
Normal file
@ -0,0 +1,27 @@
|
||||
{{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }}
|
||||
{{ define "content" }}
|
||||
<div class="flex flex-col justify-center items-center self-center max-w-lg">
|
||||
<span id="welcome" class="text-center mt-8">{{i18n "InviteInsertWelcome"}}</span>
|
||||
|
||||
<form
|
||||
id="consume"
|
||||
action="{{urlTo "complete:invite:consume"}}"
|
||||
method="POST"
|
||||
class="flex flex-col items-center self-stretch"
|
||||
>
|
||||
{{.csrfField}}
|
||||
<input type="hidden" name="invite" value={{.Token}}>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="id"
|
||||
placeholder="{{i18n "PubKeyRefPlaceholder"}}"
|
||||
class="mt-8 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate flex-auto text-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent">
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="my-8 w-32 shadow rounded px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
|
||||
>{{i18n "GenericSubmit"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
Loading…
x
Reference in New Issue
Block a user