diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f2b4c..16fe154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ * Move the infinity_one_pages dependency into the one_pages plugin * Added new Users Guide * Added new Administration Guide +* Add site avatar to admin for customizing the desktop app icon +* Add site_client_name to admin for customizing the desktop app tooltip ### Bug Fixes diff --git a/assets/css/app.scss b/assets/css/app.scss index 17080d2..86d9287 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -485,6 +485,14 @@ aside.side-nav nav.options { } } } +.dropzone.site-avatar-dropzone { + height: 125px; + width: calc(100% - 178px); +} +.dropzone.site-avatar-dropzone span { + top: calc(50% - 8px); +} +.site-avatar-dropzone, .avatar-dropzone { border: 1px dashed #ddd; border-radius: 5px; @@ -494,6 +502,7 @@ aside.side-nav nav.options { display: inline-block; text-align: center; z-index: 9999; + position: relative; &.over { border: 1px dashed #666; diff --git a/assets/js/admin.js b/assets/js/admin.js index d2d65cf..552a96d 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -6,52 +6,55 @@ console.log('loading admin'); const reset_i = '' OneChat.on_load(function(one_chat) { - one_chat.admin = new Admin(one_chat) -}) + one_chat.admin = new Admin(one_chat); +}); class Admin { constructor(one_chat) { - this.one_chat = one_chat - this.modifed = false - this.register_events(this) + this.one_chat = one_chat; + this.modifed = false; + this.register_events(this); } enable_save_button() { - let save = $('button.save') + let save = $('button.save'); if (save.attr('disabled') == 'disabled') { - save.removeAttr('disabled') - save.parent().prepend(``) - this.modified = true + save.removeAttr('disabled'); + save.parent().prepend(``); + this.modified = true; } } disable_save_button() { - let save = $('button.save') - this.modified = false - save.attr('disabled', 'disabled') - $('button.discard').remove() + let save = $('button.save'); + this.modified = false; + save.attr('disabled', 'disabled'); + $('button.discard').remove(); } add_reset_setting_button(target, input_line) { - let data_settings = target.getAttribute('name').replace('[', '__').replace(']', '') - let reset = `` - if (input_line.find('button.reset-settings').length === 0) { - input_line.append(reset); + let name = target.getAttribute('name'); + if (name) { + let data_settings = name.replace('[', '__').replace(']', ''); + let reset = ``; + if (input_line.find('button.reset-settings').length === 0) { + input_line.append(reset); - Rebel.set_event_handlers($(target).selector); + Rebel.set_event_handlers($(target).selector); + } } } handle_change(e) { let target = e.currentTarget; let input_line = $(target).closest('.input-line'); - this.enable_save_button() - input_line.addClass('setting-changed') + this.enable_save_button(); + input_line.addClass('setting-changed'); this.add_reset_setting_button(target, input_line); } register_events(admin) { $('body') .on('click', 'button.discard', () => { // admin.disable_save_button() - $('a.admin-link[data-id="admin_info"]').click() + $('a.admin-link[data-id="admin_info"]').click(); }) .on('change', '.admin-settings form input:not(.search)', (e) => { this.handle_change(e); @@ -66,36 +69,36 @@ class Admin { this.handle_change(e); }) .on('change', '.permissions-manager [type="checkbox"]', function(e, t) { - e.preventDefault() - console.log('checkbox change t', $(this)) - let name = $(this).attr('name') - let value = $(this).is(':checked') + e.preventDefault(); + console.log('checkbox change t', $(this)); + let name = $(this).attr('name'); + let value = $(this).is(':checked'); if (!value) { value = "false" } OneChat.userchan.push('admin:permissions:change:' + name, {value: value}) .receive("ok", resp => { // stop_loading_animation() - toastr.success('Room ' + name + ' updated successfully.') + toastr.success('Room ' + name + ' updated successfully.'); }) }) .on('click', '.page-settings .section button.expand', function(e) { - e.preventDefault() + e.preventDefault(); $(this) .addClass('collapse') .removeClass('expand') .first().html('Collapse') .closest('.section-collapsed') - .removeClass('section-collapsed') + .removeClass('section-collapsed'); }) .on('click', '.page-settings .section button.collapse', function(e) { - e.preventDefault() + e.preventDefault(); $(this) .removeClass('collapse') .addClass('expand') .first().html('Expand') .closest('.section-title') .parent() - .addClass('section-collapsed') + .addClass('section-collapsed'); }) .on('click', '.admin-settings button.save', function(e) { //console.log('saving form....', $('form').data('id')) @@ -103,37 +106,37 @@ class Admin { OneChat.userchan.push('admin:save:' + $('form').data('id'), $('form').serializeArray()) .receive("ok", resp => { if (resp.success) { - admin.disable_save_button() - toastr.success(resp.success) + admin.disable_save_button(); + toastr.success(resp.success); } else if (resp.error) { - toastr.error(resp.error) + toastr.error(resp.error); } }) }) .on('click', 'button.refresh', function(e) { - let page = $(this).closest('section').data('page') - $('a.admin-link[data-id="' + page + '"]').click() + let page = $(this).closest('section').data('page'); + $('a.admin-link[data-id="' + page + '"]').click(); }) .on('click', 'section.admin .list-view.channel-settings span[data-edit]', (e) => { - let channel_id = $(e.currentTarget).closest('[data-id]').data('id') - this.userchan_push('edit', {channel_id: channel_id, field: $(e.currentTarget).data('edit')}) + let channel_id = $(e.currentTarget).closest('[data-id]').data('id'); + this.userchan_push('edit', {channel_id: channel_id, field: $(e.currentTarget).data('edit')}); }) .on('click', 'section.admin .channel-settings button.save', e => { - let channel_id = $(e.currentTarget).closest('[data-id]').data('id') - let params = $('.channel-settings form').serializeArray() - this.userchan_push('save', {channel_id: channel_id, params: params}) + let channel_id = $(e.currentTarget).closest('[data-id]').data('id'); + let params = $('.channel-settings form').serializeArray(); + this.userchan_push('save', {channel_id: channel_id, params: params}); }) .on('click', 'section.admin .channel-settings button.cancel', e => { - let channel_id = $(e.currentTarget).closest('[data-id]').data('id') - this.userchan_push('cancel', {channel_id: channel_id}) + let channel_id = $(e.currentTarget).closest('[data-id]').data('id'); + this.userchan_push('cancel', {channel_id: channel_id}); }) .on('click', '#showPassword', e => { let prefix = ""; if ($('#user_password').length > 0) { prefix = "user_"; } - let password_name = `#${prefix}password` - let password_confirmation_name = `#${prefix}password_confirmation` + let password_name = `#${prefix}password`; + let password_confirmation_name = `#${prefix}password_confirmation`; $(e.currentTarget).hide(); $('#hidePassword').show(); $(password_name).attr('type','text'); @@ -144,26 +147,26 @@ class Admin { if ($('#user_password').length > 0) { prefix = "user_"; } - let password_name = `#${prefix}password` - let password_confirmation_name = `#${prefix}password_confirmation` + let password_name = `#${prefix}password`; + let password_confirmation_name = `#${prefix}password_confirmation`; $(e.currentTarget).hide(); $('#showPassword').show(); $(password_name).attr('type','password'); $(password_confirmation_name).attr('type','password'); }) .on('click', '#randomPassword', e => { - let new_password = OneChat.randomString(12) + let new_password = OneChat.randomString(12); let prefix = ""; if ($('#user_password').length > 0) { prefix = "user_"; } - let password_name = `#${prefix}password` - let password_confirmation_name = `#${prefix}password_confirmation` + let password_name = `#${prefix}password`; + let password_confirmation_name = `#${prefix}password_confirmation`; - e.preventDefault() - e.stopPropagation() - $(password_name).attr('type', 'password').val(new_password) - $(password_confirmation_name).attr('type', 'password').val(new_password) + e.preventDefault(); + e.stopPropagation(); + $(password_name).attr('type', 'password').val(new_password); + $(password_confirmation_name).attr('type', 'password').val(new_password); $('#showPassword').show(); $('#hidePassword').hide(); }) @@ -171,36 +174,35 @@ class Admin { OneChat.userchan.push('admin:save:user', $('form.user').serializeArray()) .receive("ok", resp => { if (resp.success) { - toastr.success(resp.success) - this.close_edit_form($('form.user').data('username')) + toastr.success(resp.success); + this.close_edit_form($('form.user').data('username')); } else if (resp.error) { - toastr.error(resp.error) + toastr.error(resp.error); } }) .receive("error", resp => { - console.log('error resp', resp) + console.log('error resp', resp); if (resp.error) { - toastr.error(resp.error) + toastr.error(resp.error); } if (resp.errors) { - this.show_form_errors(resp.errors) + this.show_form_errors(resp.errors); } }) }) .on('click', 'section.admin form.user button.cancel', e => { - this.close_edit_form($('form.user').data('username')) + this.close_edit_form($('form.user').data('username')); }) .on('click', 'a.new-role', e => { - OneChat.userchan.push('admin:permissions:role:new', {}) + OneChat.userchan.push('admin:permissions:role:new', {}); }) .on('click', 'a[href="#admin-permissions-edit"]', e => { - let name = $(e.currentTarget).attr('name') - console.log('permissions edit', name) - OneChat.userchan.push('admin:permissions:role:edit', {name: name}) + let name = $(e.currentTarget).attr('name'); + OneChat.userchan.push('admin:permissions:role:edit', {name: name}); }) .on('click', '.admin-role.delete', e => { - let name = $(e.currentTarget).attr('data-name') - OneChat.userchan.push('admin:permissions:role:delete', {name: name}) + let name = $(e.currentTarget).attr('data-name'); + OneChat.userchan.push('admin:permissions:role:delete', {name: name}); }) .on('click', 'a[href="/admin/permissions"]', e => { e.preventDefault(); diff --git a/assets/js/chat_dropzone.js b/assets/js/chat_dropzone.js index ebdb447..c37d45b 100644 --- a/assets/js/chat_dropzone.js +++ b/assets/js/chat_dropzone.js @@ -6,40 +6,47 @@ $(document).ready(() => { return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window; }(); if (isAdvancedUpload) { - let droppedFiles = false + let droppedFiles = false; let enterTarget = null; - let obj = $('.dropzone') + let obj = $('.dropzone'); $('body').on('drag dragstart dragend dragover dragenter dragleave drop', '.dropzone', e => { - e.preventDefault() - e.stopPropagation() + e.preventDefault(); + e.stopPropagation(); }) .on('dragover dragenter', '.dropzone', (e) => { if (chat_settings.allow_upload) { - enterTarget = e.target - $('.dropzone').addClass('over') + enterTarget = e.target; + $('.dropzone').addClass('over'); } }) .on('dragleave dragend drop', '.dropzone', (e) => { if (enterTarget == e.target) { - $('.dropzone').removeClass('over') + $('.dropzone').removeClass('over'); } }) .on('drop', '.avatar-dropzone', event => { // handle the avatar image file drop if (chat_settings.allow_upload) { - let e = event.originalEvent || event - let files = e.dataTransfer.files || [] - OneChat.fileUpload.handleAvatarUpload(files, obj) + let e = event.originalEvent || event; + let files = e.dataTransfer.files || []; + OneChat.fileUpload.handleAvatarUpload(files, obj); } }) - .on('drop', '.dropzone:not(.avatar-dropzone)', event => { - // handle the attachment file drop + .on('drop', '.site-avatar-dropzone', event => { if (chat_settings.allow_upload) { - let e = event.originalEvent || event - let files = e.dataTransfer.files || [] - OneChat.fileUpload.handleFileUpload(files, obj) + let e = event.originalEvent || event; + let files = e.dataTransfer.files || []; + OneChat.fileUpload.handleSiteAvatarUpload(files, obj); } }) + .on('drop', '.attachment-dropzone', event => { + // handle the attachment file drop + if (chat_settings.allow_upload) { + let e = event.originalEvent || event; + let files = e.dataTransfer.files || []; + OneChat.fileUpload.handleFileUpload(files, obj); + } + }); } -}) +}); diff --git a/assets/js/fileUpload.coffee.bak b/assets/js/fileUpload.coffee.bak deleted file mode 100644 index 0751564..0000000 --- a/assets/js/fileUpload.coffee.bak +++ /dev/null @@ -1,122 +0,0 @@ -readAsDataURL = (file, callback) -> - reader = new FileReader() - reader.onload = (ev) -> - callback ev.target.result, file - - reader.readAsDataURL file - -readAsArrayBuffer = (file, callback) -> - reader = new FileReader() - reader.onload = (ev) -> - callback ev.target.result, file - - reader.readAsArrayBuffer file - -fileUploadIsValidContentType = (file) -> - return true - -@fileUpload = (files) -> - roomId = "" - files = [].concat files - - consume = -> - file = files.pop() - if not file? - swal.close() - return - - readAsDataURL file.file, (fileContent) -> - if not fileUploadIsValidContentType file.file.type - swal - title: t('FileUpload_MediaType_NotAccepted') - text: file.file.type || "*.#{s.strRightBack(file.file.name, '.')}" - type: 'error' - timer: 3000 - return - - if file.file.size is 0 - swal - title: 'File Empty') - type: 'error' - timer: 1000 - return - - text = '' - - if file.type is 'audio' - text = """ -
The coolest place in the universe.<\/p>" } + |> add_realm_icon(site_avatar_url) render(conn, "server_settings.json", data: data) end + + defp add_realm_icon(data, nil), do: data + + defp add_realm_icon(data, url), do: Map.put(data, :realm_icon, url) end diff --git a/lib/infinity_one_web/controllers/site_avatar_controller.ex b/lib/infinity_one_web/controllers/site_avatar_controller.ex new file mode 100644 index 0000000..ccdfbc2 --- /dev/null +++ b/lib/infinity_one_web/controllers/site_avatar_controller.ex @@ -0,0 +1,35 @@ +defmodule InfinityOneWeb.SiteAvatarController do + @moduledoc """ + Handles SiteAvatar Uploads + + """ + use InfinityOneWeb, :controller + + alias InfinityOne.Accounts + alias InfinityOne.Settings.General + alias InfinityOne.SiteAvatar + + require Logger + + @doc """ + Create a newly uploaded SiteAvatar and saves if on the General settings schema + + Requested through Ajax only. Creates the uploaded file and assigns it + to the schema. Returns the file URL to the client for display in the + admin page. + """ + def create(conn, params) do + + general = General.get() + old_site_avatar = general.site_avatar + + case General.update(general, params) do + {:ok, general} -> + SiteAvatar.delete({old_site_avatar, nil}) + render conn, "success.json", url: SiteAvatar.url(general.site_avatar) + |> OneChatWeb.SharedView.view_url() + {:error, _} -> + render conn, "error.json", %{} + end + end +end diff --git a/lib/infinity_one_web/router.ex b/lib/infinity_one_web/router.ex index 69dcccf..6172b56 100644 --- a/lib/infinity_one_web/router.ex +++ b/lib/infinity_one_web/router.ex @@ -62,6 +62,11 @@ defmodule InfinityOneWeb.Router do post("/upload_backup", UploadController, :create) end + scope "/", InfinityOneWeb do + pipe_through(:api) + + post("/site_avatar", SiteAvatarController, :create) + end # The following is a prototype of an API implementation. It is basically # working, without authentication. Need updates in Coherence to get it # working diff --git a/lib/infinity_one_web/uploaders/site_avatar.ex b/lib/infinity_one_web/uploaders/site_avatar.ex new file mode 100644 index 0000000..35ae674 --- /dev/null +++ b/lib/infinity_one_web/uploaders/site_avatar.ex @@ -0,0 +1,47 @@ +defmodule InfinityOne.SiteAvatar do + @moduledoc """ + Arc File Upload descriptor module for SiteAvatar uploads. + """ + use Arc.Definition + use Arc.Ecto.Definition + require Logger + + def __storage, do: Arc.Storage.Local + + @versions [:default] + + @acl :public_read + + @doc """ + White list Avatar file types + + Avatars must be image files, so only image files are supported. + """ + def validate({file, _}) do + extname = file.file_name |> Path.extname |> String.downcase + ~w(.jpg .jpeg .gif .png .ico) |> Enum.member?(extname) + end + + + def transform(:default, {_, %{type: "image" <> _}} = _params) do + {:convert, "-strip -resize @1600 -format png", :png} + end + + def filename(:default, {%{file_name: name}, _}) do + name + end + + def storage_dir(_version, _) do + storage_dir() + end + + def storage_dir() do + path = "priv/static/uploads/site_avatar" + if InfinityOne.env == :prod do + Path.join(Application.app_dir(:infinity_one), path) + else + path + end + end + +end diff --git a/lib/infinity_one_web/views/site_avatar_view.ex b/lib/infinity_one_web/views/site_avatar_view.ex new file mode 100644 index 0000000..f8beceb --- /dev/null +++ b/lib/infinity_one_web/views/site_avatar_view.ex @@ -0,0 +1,18 @@ +defmodule InfinityOneWeb.SiteAvatarView do + @moduledoc """ + View related concerns for the SiteAvatar Controller. + """ + use InfinityOneWeb, :view + + @doc """ + Render json responses for the Upload avatar feature. + """ + def render("success.json", opts) do + %{success: true, url: opts[:url]} + end + + def render("error.json", _opts) do + %{error: true} + end + +end diff --git a/plugins/one_admin/lib/one_admin_web/view/utils.ex b/plugins/one_admin/lib/one_admin_web/view/utils.ex index b8612a1..43655cf 100644 --- a/plugins/one_admin/lib/one_admin_web/view/utils.ex +++ b/plugins/one_admin/lib/one_admin_web/view/utils.ex @@ -41,6 +41,37 @@ defmodule OneAdminWeb.View.Utils do end end + def file_upload_line(_f, item, field, _title, opts \\ []) do + # description = opts[:description] + dropzone = opts[:dropzone] + title = opts[:title] + + content_tag :div, class: "input-line double-col set-avatar" do + [ + content_tag :label, class: "setting-label" do + title + end, + content_tag :div, class: "setting-field" do + [ + content_tag :div, class: "avatar-full", title: title do + tag :img, src: InfinityOne.SiteAvatar.url(Map.get(item, field, "")) |> OneChatWeb.SharedView.view_url() + end, + content_tag :div, class: "file", style: "position: relative;", title: ~g"Upload File" do + content_tag :i, class: "icon-upload" do + tag :input, accept: "", type: "file" + end + end, + content_tag :div, class: "dropzone #{dropzone}-dropzone", style: "margin-top: 10px;", title: ~g"Drop file here to upload" do + content_tag :span, [], do: ~g"Drop file to upload" + end, + # hidden_input(f, field), + # |> do_description(description) + ] + end + ] + end + end + def text_input_line(f, item, field, title, opts \\ []) do type = opts[:type] || :text description = opts[:description] diff --git a/plugins/one_chat/lib/one_chat/services/channel_service.ex b/plugins/one_chat/lib/one_chat/services/channel_service.ex index dbc3e6f..5ff504e 100644 --- a/plugins/one_chat/lib/one_chat/services/channel_service.ex +++ b/plugins/one_chat/lib/one_chat/services/channel_service.ex @@ -150,7 +150,7 @@ defmodule OneChat.ChannelService do Subscription.get_by_user_id user_id, opts end - def insert_channel!(%{user_id: user_id} = params) do + def insert_channel!(%{"user_id" => user_id} = params) do user_id |> Accounts.get_user(default_preload: true) |> insert_channel!(params) diff --git a/plugins/one_chat/lib/one_chat_web/templates/admin/general.html.slime b/plugins/one_chat/lib/one_chat_web/templates/admin/general.html.slime index e99a140..1d27765 100644 --- a/plugins/one_chat/lib/one_chat_web/templates/admin/general.html.slime +++ b/plugins/one_chat/lib/one_chat_web/templates/admin/general.html.slime @@ -13,13 +13,15 @@ section.page-container.page-home.page-static.page-settings.admin-settings = unauthorized_message() - else .content.background-transparent-dark - = form_for @changeset, "#", [id: "admin-general-form", "data-id": "general"], fn f -> + = form_for @changeset, "#", [multipart: true, id: "admin-general-form", "data-id": "general"], fn f -> = hidden_input f, :id, value: item.id .rocket-form .section .section-content.border-component-color = text_input_line f, item, :site_name, ~g(Site Name), changed: @changed = text_input_line f, item, :site_url, ~g(Site Url), changed: @changed + = text_input_line f, item, :site_client_name, ~g(Site Client Name), changed: @changed, description: ~g(The name given to this site in the desktop application.) + = file_upload_line f, item, :site_avatar, ~g(Site Avatar), changed: @changed, description: ~g(Override the default icon for this site in the desktop application.), title: ~g(Upload Site Avatar), dropzone: "site-avatar" .input-line.double-col label.setting-label title="#{~g(Restart)}" = ~g(Restart) .setting-field diff --git a/plugins/one_chat/lib/one_chat_web/templates/home/index.html.slime b/plugins/one_chat/lib/one_chat_web/templates/home/index.html.slime index c1d486c..ebd294e 100644 --- a/plugins/one_chat/lib/one_chat_web/templates/home/index.html.slime +++ b/plugins/one_chat/lib/one_chat_web/templates/home/index.html.slime @@ -34,7 +34,8 @@ javascript: window.chat_settings = { link_preview: false, - use_emojis: true + use_emojis: true, + allow_upload: true }; let myPanel = $('.page-container') diff --git a/plugins/one_chat/lib/one_chat_web/templates/master/room.html.slime b/plugins/one_chat/lib/one_chat_web/templates/master/room.html.slime index 46f67f4..5fe6441 100644 --- a/plugins/one_chat/lib/one_chat_web/templates/master/room.html.slime +++ b/plugins/one_chat/lib/one_chat_web/templates/master/room.html.slime @@ -1,5 +1,5 @@ .room-container - .dropzone + .dropzone.attachment-dropzone .dropzone-overlay.background-transparent-darkest.color-content-background-color .background-transparent-darkest = ~g"Drop to upload file" diff --git a/priv/repo/migrations/20180330014650_add_site_avatar_to_settings_general.exs b/priv/repo/migrations/20180330014650_add_site_avatar_to_settings_general.exs new file mode 100644 index 0000000..0270857 --- /dev/null +++ b/priv/repo/migrations/20180330014650_add_site_avatar_to_settings_general.exs @@ -0,0 +1,10 @@ +defmodule InfinityOnePages.Repo.Migrations.AddSiteAvatarToSettingsGeneral do + use Ecto.Migration + + def change do + alter table(:settings_general) do + add :site_avatar, :string + add :site_client_name, :string, default: "use-host-name" + end + end +end