Skip to content
128 changes: 128 additions & 0 deletions static/js/InviteMember.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Functionality for "Invite A New Member" form shown in the sidebar of a shake
* the current user is an editor of. Lets the user type in a few characters, see
* an autocompleted list of matching usernames to pick from, and finally sends
* an invitation via the backend.
*/

// Invite Member widget for the Shake administrator.
const $mainModule = $("#shake-invite-member");
const $inputField = $mainModule.find(".input-text");
const $inviteButton = $mainModule.find(".invite-button");
const $shakeResults = $mainModule.find(".shake-results");
const $form = $mainModule.find("form");
const $title = $mainModule.find("h3");
let searchResults = [];
let lastSearch = "";

const InviteMember = {
attachEvents: function () {
$inputField.keyup((ev) => this.searchNames(ev));
$form.submit((ev) => this.submitForm());
$shakeResults.click((ev) => this.selectUser($(ev.target).text()));
$inviteButton.click(() => {
this.sendInvite();
return false;
});
},

searchNames: async function () {
if ($inputField.val() == "") {
this.clearResults();
this.clearInput();
return false;
}

// don't search again if field hasn't changed.
if ($inputField.val() == lastSearch) {
return false;
}
lastSearch = $inputField.val();

const data = $form.serialize();
const resp = await fetch("/account/quick_name_search", {
method: "POST",
body: new URLSearchParams(data),
});
const json = await resp.json();

if ("users" in json) {
this.updateResults(json["users"]);
}
},

updateResults: function (users) {
searchResults = users;
if (searchResults.length == 0) {
this.clearResults();
} else {
this.renderResults();
}
},

renderResults: function () {
$shakeResults.html("").show();
for (let i = 0; i < searchResults.length; i++) {
$shakeResults.append(
`<li>
<img src="${searchResults[i].profile_image_url}"
width="24" height="24">
<span>${searchResults[i].name}</span>
</li>`,
);
}
},

selectUser: function (userName) {
this.clearResults();
$inputField.val(userName);
$inviteButton.removeAttr("disabled");
},

submitForm: function (ev) {
if (
searchResults.length == 1 &&
searchResults[0].name == $inputField.val()
) {
this.selectUser(searchResults[0].name);
this.sendInvite();
this.clearResults();
}
return false;
},

clearResults: function () {
lastSearch = "";
$shakeResults.hide().html("");
},

clearInput: function () {
$inputField.val("");
$inviteButton.attr("disabled", "disabled");
},

sendInvite: async function () {
if ($inviteButton.disabled) {
return false;
} else {
const url = $form.attr("action");
const data = $form.serialize();

await fetch(url, {
method: "POST",
body: new URLSearchParams(data),
});

this.dataSent();
return false;
}
},

dataSent: function () {
$title.html("Your invitation has been sent");
this.clearInput();
this.clearResults();
},
};

export { InviteMember };
65 changes: 65 additions & 0 deletions static/js/NSFWCover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { applyHoverForVideo } from "./common.js";

/**
* Functionality associated with NSFW covers. Called on to attach event
* handlers to any posts that the server has generated a NSFW cover for.
*/

const NSFWCover = {
attachEvents($root) {
// Per invocation functions that will close over the context dependent
// variables defined above.
function clickShowImage(ev) {
var location = document.location,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change all the remaining vars to lets?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will definitely do this, and make it an eslint rule. Prob left this as wasn't changing the $.get just yet and trying to minimise diffs.

basePath = location.protocol + "//" + location.host,
filePath = $(ev.target).attr("href");
// Going to leave this as a jquery get rather than migrate to fetch
// right away. The two implementations behave differently and only
// the existing method seems to work with /services/oembed
$.get(
basePath +
"/services/oembed?include_embed=1&url=" +
escape(basePath + filePath),
(resp) => loadImage(resp),
"json",
);
return false;
}

function loadImage(response) {
var parent = $root.parent(),
parentHeight = parent.height();

if (response["type"] === "photo") {
parent
.css("min-height", parentHeight + "px")
.html(
'<img class="unsized" src="' + response["url"] + '">',
);
} else if (response["embed_html"]) {
parent
.css("min-height", parentHeight + "px")
.html(
'<div class="data-wrapper">' +
response["embed_html"] +
"</div>",
);
} else if (response["type"] === "video") {
var content = parent
.css("min-height", parentHeight + "px")
.html(
response["html"].replace(
/<source /g,
'<source onerror="fallbackImage(this)" ',
),
);
applyHoverForVideo(content.find("video.autoplay"));
}
}

// Attach any event handlers.
$root.delegate("a", "click", (ev) => clickShowImage(ev));
},
};

export { NSFWCover };
200 changes: 200 additions & 0 deletions static/js/NewPostPanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* Functionality associated with the new post panel that pops up on any page
* allowing the user to post a new image or video to one of their shakes.
*
* Takes care of adding event listeners to the new post button, as well as to
* the shake dropdowns on both the image and video sides of the dialog. The new
* post dropdown slides in from the top of the screen.
*/
let $newPostPanel;
let $newPostPanelInner;
let $newPostButton;
let $saveVideoForm;
let $saveVideoFormButton;
let $postVideoForm;
let $postVideoFormButton;
let $uploadImageInput;
let $linkToVideo;
let $videoShakeId;
let $shakeSelector;

function removeEvents() {
$saveVideoFormButton.unbind();
$postVideoFormButton.unbind();
$shakeSelector.unbind();
$linkToVideo.unbind();
}

function initDom() {
$newPostPanel = $("#new-post-panel");
$newPostPanelInner = $("#new-post-panel .new-post-panel--inner");
$newPostButton = $("#new-post-button");
// upload image
$uploadImageInput = $("#upload-image-input");
// link to video
$linkToVideo = $("#link-to-video");
$videoShakeId = $("#video-shake-id");
// video preview screen
$saveVideoForm = $("#new-post-panel .save-video-form");
$saveVideoFormButton = $("#new-post-panel .save-video-form .btn");
$postVideoForm = $("#new-post-panel .post-video-form");
$postVideoFormButton = $("#new-post-panel .post-video-form .btn");
// shake selector
$shakeSelector = $(".shake-selector");
}

function initEvents() {
// The events that are inside the panel that we want to initialize
// when the panel loads. These are the events that are subject
// to change depending on content that is loaded.

// Video upload step 1.
// Called when user clicks "video" link and takes them to a new form where
// they can enter the video url.
$linkToVideo.click(function () {
NewPostPanel.loadPostVideo();
return false;
});

// Video upload step 2.
// Called when the user clicks "Go get it!" button which takes the user to a
// new form containing a preview of the video.
$saveVideoFormButton.click(function (e) {
NewPostPanel.submitSaveVideo();
return false;
});

// Video upload step 3.
// Called when the user clicks "Yes! Post it please!" which uploads the
// video via submitPostVideo().
$postVideoFormButton.click(function (e) {
NewPostPanel.submitPostVideo();
return false;
});

// Uploads the image file upon selection by the file upload dialog.
$uploadImageInput.change(function () {
$(this).closest("form").submit();
});

$shakeSelector.click(NewPostPanel.toggleShakeSelector);
$shakeSelector.find("ul a").click(NewPostPanel.chooseShake);
}

const NewPostPanel = {
attachEvents: function () {
initDom();

$newPostButton.click(function () {
NewPostPanel.loadNewPost();
return false;
});

// We don't want click event on panel to bubble up to body
// since a click to body closes the panel.
$newPostPanel.click(function (ev) {
ev.stopPropagation();
});
},

toggleShakeSelector: function (ev) {
$(this).toggleClass("is-active").find("ul").toggle();
ev.stopPropagation();
ev.preventDefault();
},

// Sets the text of the shake to the chosen one and
// sets a hidden input field with the proper shake id.
chooseShake: function () {
const $shakeSelector = $(this).parents(".shake-selector");
const $selectedShake = $shakeSelector.find(".green");
const $selectedShakeInput = $shakeSelector.find("input");
const name = $(this).html();
const id = $(this)
.attr("id")
.replace(/[^0-9]+/, "");
$selectedShake.html(name);
$selectedShakeInput.val(id);
},

// Renders step 1 of image / video upload process - shake choice and file
// type.
loadNewPost: async function () {
var url = "/tools/new-post";
const resp = await fetch(url);

this.refreshPanel(await resp.text());
this.expandPanel();
return false;
},

// Renders step 2 of the video upload process - entering the url.
loadPostVideo: async function () {
let shakeSuffix = "";
if ($videoShakeId.length > 0) {
shakeSuffix = "?shake_id=" + $videoShakeId.val();
}
const url = `/tools/save-video${shakeSuffix}`;

const resp = await fetch(url);
this.refreshPanel(await resp.text());
this.expandPanel();
},

expandPanel: function () {
$newPostPanel.slideDown();
var that = this;
$("body").one("click", $.proxy(this.close_panel, this));
// we want to hide anything with a video since we can't
// overlap things like youtube embeds, which is an iframe
// that has an absolutely positioned flash element inside.
$(".the-image iframe").each(function () {
$(this).parent().css("height", $(this).height());
$(this).parent().css("width", $(this).width());
$(this).hide();
});
},

close_panel: function () {
$newPostPanel.hide();
removeEvents();
// show the videos again.
$(".the-image iframe").show();
},

// Renders step 3 of the video upload process - previewing the video.
submitSaveVideo: async function () {
const url = $saveVideoForm.attr("action");
const data = $saveVideoForm.serialize();

const resp = await fetch(`${url}?${new URLSearchParams(data)}`);
this.refreshPanel(await resp.text());
},

// Final step of video upload - submitting the post details to the server.
submitPostVideo: async function () {
const url = $postVideoForm.attr("action");
const data = $postVideoForm.serialize();
$postVideoFormButton.unbind("click").find("span").html("Posting...");

const resp = await fetch(url, {
method: "POST",
body: new URLSearchParams(data),
});
const json = await resp.json();

// Redirect to the new post permalink page.
document.location =
document.location.protocol +
`//${document.location.host}${json["path"]}`;
},

refreshPanel: function (html) {
$newPostPanelInner.html(html);
removeEvents();
initDom();
initEvents();
},
};

export { NewPostPanel };
Loading