gary-kim / riotchat

Element for Nextcloud
https://apps.nextcloud.com/apps/riotchat
GNU Affero General Public License v3.0
125 stars 22 forks source link

File upload/link from Nextcloud #120

Open plague69 opened 4 years ago

plague69 commented 4 years ago

Would be nice if Nextcloud files system can be tied in somehow.

Auto share link create or better yet embed content from Nextcloud

WebDav?

Iwios commented 1 year ago

Hello, I'm very interesting by this feature.

For me is the possibilities to upload file from Nextcloud. This is possible ? Or the core of synapse matrix not allowed this kind of features ?

Thatoo commented 1 year ago

I would even like the "upload file" button (in Element desktop app) to be replaced (hidden or optionally removed) by a "share file from nextcloud" button that would not upload anything to the synapse server but only gives access to a way to copy/paste a nextcloud file's share link in the chat.

This should not be too difficult trick to make in Element code.

Thatoo commented 1 year ago

or [ as said by plague69], better yet embed content from Nextcloud. However, this would require to add a specific widget to each room. The work is much more important. It needs to develop a widget and then to find a way for Element to add this widget to all rooms. I'm not sure this last part would be possible.

Thatoo commented 1 year ago

And of course, ideally, it would be nice if in the file app, the share button could allow to share the file to the Element app also. I guess it is an issue to open in the files app repo.

Thatoo commented 1 year ago

@gary-kim To implement this feature, does it require this work to be done https://github.com/gary-kim/riotchat/pull/369 ? and thus, does it require either

Thatoo commented 1 year ago

As it doesn't look like neither Matrix will allow soon subpath neither will Nextcloud accept to add riotchat to rootUrlApps, could we move an other way, not as awesome as the integration Sorunome was working on but still something better than the current option.

Would it be possible to replace the "Element join file" button that only let the user upload a file from his/her computer by a "Nextcloud join file" such as the one in spreed that allow the user to either upload a file from computer or share from Nextcloud (let's forget about other option). If having the choice between the two is too complicated, I'd rather have only a "share from nextcloud" button than the current "upload from computer" button.

image

image

or like in mail app. I like actually more the mail app that gives three choices, upload file from computer, share file from nextcloud or paste a share link to a file

image

Thatoo commented 7 months ago

It could be an integration of Nextcloud picker : https://github.com/nextcloud/picker

Thatoo commented 5 months ago

Here is how it could look like thanks to my work on picker : https://github.com/Thatoo/picker/tree/InternalAndClipboard

image

image

image

And once you click on "Copy file link", it copies the link and close the iframe.

An idea I have to move toward a more efficient tool would be to remove all three button "Cancel" "Open file" "Copy link and close window"

and to make the three options "Read", "Write" and "Internal link" becoming button that directly copy the link and close the window, and even ideally move these three buttons to the first page.

Something lik image

However, I didn't want to be that radical in my proposal so I keep it the way it is working now and just added a new option to get the Internal link and a button to copy link and close window keeping the original button that open the file (I just renamed it to be more comprehensible).

Maybe I'll work on that idea later on.

Thatoo commented 5 months ago

My only issue right now is how to display this nice nextcloud logo like that image

[edit] I guess, it would even better with this icon image

So far, I did a javascript trick which is not very nice. It would be much better to be able to add it when the app compile but I don't know how to do that. Then this icon should display only if activated in riotchat settings (with the url picker).

Here is my javascript trick to add in js/main.js file (not recommended, just for a proof of concept) :

const iframe = document.getElementById('riot-iframe')

if (iframe) {
  const interval = setInterval(() => {
    const titles = iframe.contentWindow.document.querySelector(".mx_RoomSublist_tiles")
    const actions = iframe.contentWindow.document.querySelector(".mx_MessageComposer_actions")

    if (titles) {
      setupClickHandler(iframe, titles)
      clearTimeout(interval)
    }

    if (actions) {
      addButton(iframe.contentWindow.document)
    }
  }, 1000)
}

function setupClickHandler(iframe, titles) {
  const iframeDocument = iframe.contentWindow.document
  titles.addEventListener('click', function(event) {
    const roomTile = event.target.closest('.mx_RoomTile')

    if (roomTile) {
      addButton(iframeDocument)
    }
  })
}

function addButton(iframeDocument) {
  const interval = setInterval(() => {
    const button = iframeDocument.querySelector(".mx_MessageComposer_upload");

    if (button) {
      const newButton = iframeDocument.createElement('a');
      newButton.setAttribute('aria-label', 'Nextcloud Share Link');
      newButton.style.marginLeft = '0';
      newButton.style.display = 'flex';
      newButton.style.alignItems = 'center';
      newButton.style.justifyContent = 'center';

      const svg = iframeDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('width', '26');
      svg.setAttribute('height', '26');
      svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');

      const path = iframeDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
      path.setAttribute('d', 'M12.018 6.537c-2.5 0-4.6 1.712-5.241 4.015-.56-1.232-1.793-2.105-3.225-2.105A3.569 3.569 0 0 0 0 12a3.569 3.569 0 0 0 3.552 3.553c1.432 0 2.664-.874 3.224-2.106.641 2.304 2.742 4.016 5.242 4.016 2.487 0 4.576-1.693 5.231-3.977.569 1.21 1.783 2.067 3.198 2.067A3.568 3.568 0 0 0 24 12a3.569 3.569 0 0 0-3.553-3.553c-1.416 0-2.63.858-3.199 2.067-.654-2.284-2.743-3.978-5.23-3.977zm0 2.085c1.878 0 3.378 1.5 3.378 3.378 0 1.878-1.5 3.378-3.378 3.378A3.362 3.362 0 0 1 8.641 12c0-1.878 1.5-3.378 3.377-3.378zm-8.466 1.91c.822 0 1.467.645 1.467 1.468s-.644 1.467-1.467 1.468A1.452 1.452 0 0 1 2.085 12c0-.823.644-1.467 1.467-1.467zm16.895 0c.823 0 1.468.645 1.468 1.468s-.645 1.468-1.468 1.468A1.452 1.452 0 0 1 18.98 12c0-.823.644-1.467 1.467-1.467z');
      path.setAttribute('fill', '#656c76');

      svg.appendChild(path);
      newButton.appendChild(svg);

      newButton.onclick = () => {
        // Create the iframe element
        var pickerFrame = document.createElement("IFRAME");
        pickerFrame.id = "pickerFrame";
        pickerFrame.height = "800"; // set the height
        pickerFrame.width = "600"; // set the width
        pickerFrame.src = "/nextcloud/apps/picker/single-link"; // Set the source URL
        pickerFrame.style.border = 'none'; // Remove the default border
        // Set the iframe styles to position it in the middle and on top
        pickerFrame.style.position = 'fixed';
        pickerFrame.style.top = '50%';
        pickerFrame.style.left = '50%';
        pickerFrame.style.transform = 'translate(-50%, -50%)';
        pickerFrame.style.zIndex = '9999'; // Adjust this value as needed
        // Append the iframe to the document body
        document.body.appendChild(pickerFrame);
//        pickerWindow = window.open('/nextcloud/apps/picker/single-link', 'pickerFrame');
      };

      newButton.addEventListener('mouseover', () => {
        path.setAttribute('fill', '#ebeef2');
      });

      newButton.addEventListener('mouseout', () => {
        path.setAttribute('fill', '#656c76');
      });

      button.insertAdjacentElement('afterend', newButton);
      clearInterval(interval);
    }
  }, 100);
}
Thatoo commented 5 months ago

I have been told to use skinning to modify the component from a modified element-web; https://github.com/element-hq/element-web/blob/develop/docs/customisations.md#custom-components but I don't know how to do that.

I asked if it was deprecated and I have been answered : Yes but you could wire it similarly directly in Webpack using https://webpack.js.org/plugins/normal-module-replacement-plugin/ which is what customisations uses

I have no idea yet what all that means. Any help would be appreciated.

gary-kim commented 5 months ago

Wow, that's really exciting!

Feel free to make a PR with the draft you have right now.

We do build Element Web from source so we can easily add things during build including in the webpack config. I should have some more time to look at it now.

Thatoo commented 4 months ago

Thanks but I first need to end the job on Picker before making a PR here. And I'm not good enough at Vue to end the job for now. If someone wants to help me, I'd be happy to end this job. I describe where I stuck here : https://github.com/nextcloud/picker/issues/34#issuecomment-2160669006 My code is here : https://github.com/Thatoo/picker/tree/ClipboardOnly

Thatoo commented 2 months ago

@gary-kim Finally my final code is here and I'm happy with it https://github.com/Thatoo/picker/tree/one_page and my picker PR is here https://github.com/nextcloud/picker/pull/47

So if you want to try it, you can git clone https://github.com/Thatoo/picker/tree/one_page into your Nextcloud apps folder, build it and then, here is the code I was using to test it in Element for Nextcloud that you could add to riotchat/js/main.js :

/*! For license information please see main.js.LICENSE.txt */
(()=>{"use strict";const o=(o,t,n)=>{const e=Object.assign({escape:!0},n||{});return"/"!==o.charAt(0)&&(o="/"+o),i=(i=t||{})||{},o.replace(/{([^{}]*)}/g,(function(o,t){const n=i[t];return e.escape?encodeURIComponent("string"==typeof n||"number"==typeof n?n.toString():o):"string"==typeof n||"number"==typeof n?n.toString():o}));var i},t=(t,n,e)=>{var i,a,c;const r=Object.assign({noRewrite:!1},e||{}),l=null!=(i=null==e?void 0:e.baseURL)?i:function(){let o=window._oc_webroot;if(typeof o>"u"){o=location.pathname;const t=o.indexOf("/index.php/");if(-1!==t)o=o.slice(0,t);else{const t=o.indexOf("/",1);o=o.slice(0,t>0?t:void 0)}}return o}();return!0!==(null==(c=null==(a=null==window?void 0:window.OC)?void 0:a.config)?void 0:c.modRewriteWorking)||r.noRewrite?l+"/index.php"+o(t,n,e):l+o(t,n,e)};function n(o,t,n){const e=document.querySelector(`#initial-state-${o}-${t}`);if(null===e){if(void 0!==n)return n;throw new Error(`Could not find initial state ${t} of ${o}`)}try{return JSON.parse(atob(e.value))}catch(n){throw new Error(`Could not parse initial state ${t} of ${o}`)}}document.addEventListener("DOMContentLoaded",(function(){i=document.title,e=document.getElementById("riot-iframe"),window.location.hash||"true"!==n("riotchat","disable_custom_urls")||window.localStorage.getItem("mx_user_id")?e.src=t("/apps/riotchat/riot/")+window.location.hash:(e.src=t("/apps/riotchat/riot/")+"#/login",window.location.hash="#/login"),e.onload=a}));let e,i="";function a(){new MutationObserver(r).observe(e.contentWindow.document.querySelector("title"),{childList:!0,attributes:!0,characterData:!0}),e.contentWindow.onhashchange=c,window.onhashchange=()=>{e.contentWindow.location.hash!==window.location.hash&&(e.contentWindow.location.hash=window.location.hash)},"true"!==n("riotchat","sso_force_iframe")&&(e.contentWindow.localStorage.__proto__.setItem=function(){for(var o=arguments.length,n=new Array(o),i=0;i<o;i++)n[i]=arguments[i];"mx_sso_hs_url"===n[0]&&"#/login"===e.contentWindow.location.hash&&(window.location.href=t("/apps/riotchat/riot/#/login")),window.localStorage.setItem.apply(this,n)})}function c(){window.location.hash!==e.contentWindow.location.hash&&(window.location.hash=e.contentWindow.location.hash)}function r(){document.title=e.contentWindow.document.title+" - "+i}})();
//# sourceMappingURL=main.js.map
const iframe = document.getElementById('riot-iframe')

if (iframe) {
  const interval = setInterval(() => {
    const titles = iframe.contentWindow.document.querySelector(".mx_RoomSublist_tiles")
    const actions = iframe.contentWindow.document.querySelector(".mx_MessageComposer_actions")

    if (titles) {
      setupClickHandler(iframe, titles)
      clearTimeout(interval)
    }

    if (actions) {
      addButton(iframe.contentWindow.document)
    }
  }, 1000)
}

function setupClickHandler(iframe, titles) {
  const iframeDocument = iframe.contentWindow.document
  titles.addEventListener('click', function(event) {
    const roomTile = event.target.closest('.mx_RoomTile')

    if (roomTile) {
      addButton(iframeDocument)
    }
  })
}

function addButton(iframeDocument) {
  const interval = setInterval(() => {
    const button = iframeDocument.querySelector(".mx_MessageComposer_upload");

    if (button) {
      const newButton = iframeDocument.createElement('a');
      newButton.setAttribute('aria-label', 'Nextcloud Share Link');
      newButton.style.marginLeft = '0';
      newButton.style.display = 'flex';
      newButton.style.alignItems = 'center';
      newButton.style.justifyContent = 'center';

      const svg = iframeDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('width', '26');
      svg.setAttribute('height', '26');
      svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');

      const path = iframeDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
      path.setAttribute('d', 'M 2.4 1.2 A 2.4 2.4 0 0 0 0 3.6 L 0 20.4 A 2.4 2.4 0 0 0 2.4 22.8 L 21.6 22.8 A 2.4 2.4 0 0 0 24 20.4 L 24 7.2 A 2.4 2.4 0 0 0 21.6 4.8 L 13.692012 4.8 L 10.8 1.9079883 A 2.4 2.4 0 0 0 9.1079883 1.2 L 2.4 1.2 z M 12.015293 9.1495898 C 14.128085 9.1487404 15.902746 10.587886 16.45834 12.528223 C 16.941727 11.501136 17.973094 10.772227 19.176035 10.772227 A 3.0319929 3.0319929 0 0 1 22.194434 13.790625 A 3.0311433 3.0311433 0 0 1 19.176035 16.809023 C 17.973944 16.809023 16.942602 16.080963 16.459219 15.053027 C 15.902774 16.993365 14.128085 18.43166 12.015293 18.43166 C 9.8914562 18.43166 8.1066027 16.97725 7.5620508 15.019922 C 7.0863114 16.066548 6.0396784 16.809023 4.8231445 16.809023 A 3.0319929 3.0319929 0 0 1 1.8055664 13.790625 A 3.0319929 3.0319929 0 0 1 4.8231445 10.772227 C 6.0396784 10.772227 7.0871316 11.51388 7.5628711 12.560508 C 8.107423 10.604028 9.8914562 9.1495898 12.015293 9.1495898 z M 12.015293 10.920879 C 10.420718 10.920879 9.1464258 12.195199 9.1464258 13.790625 A 2.856139 2.856139 0 0 0 12.015293 16.660371 C 13.610721 16.660371 14.885039 15.38605 14.885039 13.790625 C 14.885039 12.195199 13.610721 10.920879 12.015293 10.920879 z M 4.8231445 12.543516 L 4.8231445 12.544336 C 4.1239774 12.544336 3.5768555 13.091459 3.5768555 13.790625 A 1.2335258 1.2335258 0 0 0 4.8231445 15.037734 C 5.5223118 15.036884 6.069375 14.489792 6.069375 13.790625 C 6.069375 13.091459 5.5214622 12.543516 4.8231445 12.543516 z M 19.176035 12.543516 L 19.176035 12.544336 C 18.476868 12.544336 17.929746 13.091459 17.929746 13.790625 A 1.2335258 1.2335258 0 0 0 19.176035 15.037734 C 19.875202 15.037734 20.423145 14.489792 20.423145 13.790625 C 20.423145 13.091459 19.875202 12.543516 19.176035 12.543516 z ');
      path.setAttribute('fill', '#656c76');

      svg.appendChild(path);
      newButton.appendChild(svg);

      newButton.onclick = () => {
        // Create the iframe element
        var pickerFrame = document.createElement("iframe");
        pickerFrame.id = "pickerFrame";
        pickerFrame.height = 800; // set the height
        pickerFrame.width = 800; // set the width
        pickerFrame.src = "/apps/picker/single-link?option=Clipboard"; // Set the source URL
        pickerFrame.style.border = 'none'; // Remove the default border
        // Set the iframe styles to position it in the middle and on top
        pickerFrame.style.position = 'fixed';
        pickerFrame.style.top = '50%';
        pickerFrame.style.left = '50%';
        pickerFrame.style.transform = 'translate(-50%, -50%)';
        pickerFrame.style.zIndex = 9999; // Adjust this value as needed
        // Append the iframe to the document body
        document.body.appendChild(pickerFrame);
//        pickerWindow = window.open('/nextcloud/apps/picker/single-link', 'pickerFrame');
      };

      newButton.addEventListener('mouseover', () => {
        path.setAttribute('fill', '#ebeef2');
      });

      newButton.addEventListener('mouseout', () => {
        path.setAttribute('fill', '#656c76');
      });

      button.insertAdjacentElement('afterend', newButton);
      clearInterval(interval);
    }
  }, 100);
}
function closePickerIframe() {
  const pickerFrame = document.getElementById('pickerFrame');
  document.body.removeChild(pickerFrame); // Remove the iframe
}
Thatoo commented 2 months ago
function closePickerIframe() {
  const pickerFrame = document.getElementById('pickerFrame');
  document.body.removeChild(pickerFrame); // Remove the iframe
}

could remain in riotchat/js/main.js file. However, the way I insert the button isn't good (not secure). The button should be added when building Element Web from source. The button function is easy, it should simply open an iframe as I wrote it and I suggest an svg icon for it but I really don't know how to add this button at the right place when building Element Web from source.

Thatoo commented 2 months ago

If you know how to add the button, we could either hope my PR get merged and invite Element for Nextcloud users to install also the picker app, or, I could make a PR if you want to include all the code of the picker app within Element for Nextcloud app.

Thatoo commented 1 month ago

Nextcloud Picker app is ready to be used by Element for Nextcloud : https://github.com/nextcloud/picker/releases/tag/v1.0.11 I'm starting a PR (a draft) : https://github.com/gary-kim/riotchat/pull/656